Community extensions must be placed in ~/.pi/agent/extensions/, not ~/.gsd/agent/extensions/ which is reserved for bundled extensions synced from the gsd-pi package. Extensions placed in the wrong path are silently ignored by the loader. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d25dbb15fd
commit
50fbd0a837
6 changed files with 97 additions and 16 deletions
|
|
@ -7,9 +7,11 @@ description: Create, debug, and iterate on GSD extensions (TypeScript modules th
|
|||
|
||||
**Extensions are TypeScript modules** that hook into GSD's runtime (built on pi). They export a default function receiving `ExtensionAPI` and use it to subscribe to events, register tools/commands/shortcuts, and interact with the session.
|
||||
|
||||
**GSD extension paths:**
|
||||
- Global extensions: `~/.gsd/agent/extensions/*.ts` or `~/.gsd/agent/extensions/*/index.ts`
|
||||
- Project-local extensions: `.gsd/extensions/*.ts` or `.gsd/extensions/*/index.ts`
|
||||
**GSD extension paths (community/user-installed extensions):**
|
||||
- Global: `~/.pi/agent/extensions/*.ts` or `~/.pi/agent/extensions/*/index.ts`
|
||||
- Project-local: `.gsd/extensions/*.ts` or `.gsd/extensions/*/index.ts`
|
||||
|
||||
Note: `~/.gsd/agent/extensions/` is reserved for bundled extensions synced from the gsd-pi package. Community extensions placed there are silently ignored by the loader.
|
||||
|
||||
**The three primitives:**
|
||||
1. **Events** — Listen and react (`pi.on("event", handler)`). Can block tool calls, modify messages, inject context.
|
||||
|
|
|
|||
|
|
@ -26,11 +26,12 @@ Non-negotiable rules and common gotchas when building GSD extensions.
|
|||
</common_patterns>
|
||||
|
||||
<gsd_paths>
|
||||
**GSD extension paths:**
|
||||
- Global: `~/.gsd/agent/extensions/*.ts`
|
||||
- Global (subdir): `~/.gsd/agent/extensions/*/index.ts`
|
||||
**GSD extension paths (community/user-installed extensions):**
|
||||
- Global: `~/.pi/agent/extensions/*.ts`
|
||||
- Global (subdir): `~/.pi/agent/extensions/*/index.ts`
|
||||
- Project-local: `.gsd/extensions/*.ts`
|
||||
- Project-local (subdir): `.gsd/extensions/*/index.ts`
|
||||
|
||||
The upstream pi docs reference `~/.pi` paths — GSD uses `~/.gsd` everywhere instead.
|
||||
Note: `~/.gsd/agent/extensions/` is reserved for bundled extensions synced from the gsd-pi package.
|
||||
Community extensions placed there are silently ignored by the loader.
|
||||
</gsd_paths>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ Read the reference file for the specific capability being added:
|
|||
## Step 1: Identify the Extension
|
||||
|
||||
Locate the existing extension file. Check:
|
||||
- `~/.gsd/agent/extensions/` (global)
|
||||
- `~/.pi/agent/extensions/` (global community extensions)
|
||||
- `.gsd/extensions/` (project-local)
|
||||
|
||||
Read the current extension code to understand its structure.
|
||||
|
|
@ -28,7 +28,7 @@ If the extension needs new imports, add them at the top of the file.
|
|||
## Step 3: Handle Structural Changes
|
||||
|
||||
**Single file → Directory**: If the extension is outgrowing a single file:
|
||||
1. Create `~/.gsd/agent/extensions/my-extension/`
|
||||
1. Create `~/.pi/agent/extensions/my-extension/`
|
||||
2. Move the file to `index.ts`
|
||||
3. Extract helpers to separate files
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
## Step 1: Determine Scope and Placement
|
||||
|
||||
Ask the user:
|
||||
- **Global** (`~/.gsd/agent/extensions/`) — Available in all GSD sessions
|
||||
- **Global** (`~/.pi/agent/extensions/`) — Available in all GSD sessions
|
||||
- **Project-local** (`.gsd/extensions/`) — Available only in this project
|
||||
|
||||
## Step 2: Determine Extension Capabilities
|
||||
|
|
@ -36,12 +36,12 @@ Identify what the extension needs from the user's description:
|
|||
|
||||
**Single file** — for small extensions (1-2 tools/commands, simple hooks):
|
||||
```
|
||||
~/.gsd/agent/extensions/my-extension.ts
|
||||
~/.pi/agent/extensions/my-extension.ts
|
||||
```
|
||||
|
||||
**Directory with index.ts** — for multi-file extensions:
|
||||
```
|
||||
~/.gsd/agent/extensions/my-extension/
|
||||
~/.pi/agent/extensions/my-extension/
|
||||
├── index.ts
|
||||
├── tools.ts
|
||||
└── utils.ts
|
||||
|
|
@ -49,7 +49,7 @@ Identify what the extension needs from the user's description:
|
|||
|
||||
**Package with dependencies** — when npm packages are needed:
|
||||
```
|
||||
~/.gsd/agent/extensions/my-extension/
|
||||
~/.pi/agent/extensions/my-extension/
|
||||
├── package.json
|
||||
├── src/index.ts
|
||||
└── node_modules/
|
||||
|
|
|
|||
|
|
@ -32,12 +32,14 @@ gsd -e ./path/to/extension.ts
|
|||
|
||||
## Step 3: Verify File Location
|
||||
|
||||
Extensions must be in auto-discovery paths:
|
||||
- `~/.gsd/agent/extensions/*.ts`
|
||||
- `~/.gsd/agent/extensions/*/index.ts`
|
||||
Community extensions must be in auto-discovery paths:
|
||||
- `~/.pi/agent/extensions/*.ts`
|
||||
- `~/.pi/agent/extensions/*/index.ts`
|
||||
- `.gsd/extensions/*.ts`
|
||||
- `.gsd/extensions/*/index.ts`
|
||||
|
||||
Note: `~/.gsd/agent/extensions/` is reserved for bundled extensions synced from the gsd-pi package.
|
||||
|
||||
The file must `export default function(pi: ExtensionAPI) { ... }`.
|
||||
|
||||
## Step 4: Check for Common Mistakes
|
||||
|
|
|
|||
76
src/tests/create-gsd-extension-paths.test.ts
Normal file
76
src/tests/create-gsd-extension-paths.test.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
/**
|
||||
* Validates that the create-gsd-extension skill documentation uses the correct
|
||||
* community extension install path (~/.pi/agent/extensions/) instead of the
|
||||
* bundled-only path (~/.gsd/agent/extensions/).
|
||||
*
|
||||
* Bug: https://github.com/gsd-build/gsd-2/issues/3131
|
||||
*
|
||||
* ~/.gsd/agent/extensions/ is reserved for bundled extensions synced from
|
||||
* the gsd-pi package. Community/user extensions must use ~/.pi/agent/extensions/.
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const skillDir = join(__dirname, "..", "resources", "skills", "create-gsd-extension");
|
||||
|
||||
function readSkillFile(relativePath: string): string {
|
||||
return readFileSync(join(skillDir, relativePath), "utf-8");
|
||||
}
|
||||
|
||||
// All documentation files that reference community extension paths
|
||||
const docsToCheck: { file: string; label: string }[] = [
|
||||
{ file: "SKILL.md", label: "SKILL.md" },
|
||||
{ file: "references/key-rules-gotchas.md", label: "key-rules-gotchas.md" },
|
||||
{ file: "workflows/add-capability.md", label: "add-capability.md" },
|
||||
{ file: "workflows/create-extension.md", label: "create-extension.md" },
|
||||
{ file: "workflows/debug-extension.md", label: "debug-extension.md" },
|
||||
];
|
||||
|
||||
test("create-gsd-extension docs use ~/.pi/agent/extensions/ for community extensions", async (t) => {
|
||||
for (const { file, label } of docsToCheck) {
|
||||
await t.test(`${label} references ~/.pi/agent/extensions/ for global extensions`, () => {
|
||||
const content = readSkillFile(file);
|
||||
|
||||
// The doc should reference ~/.pi/agent/extensions/ (community path)
|
||||
assert.ok(
|
||||
content.includes("~/.pi/agent/extensions/"),
|
||||
`${label} should reference ~/.pi/agent/extensions/ for community extensions`,
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test("create-gsd-extension docs do NOT direct users to install in ~/.gsd/agent/extensions/", async (t) => {
|
||||
for (const { file, label } of docsToCheck) {
|
||||
await t.test(`${label} does not tell users to place extensions in ~/.gsd/agent/extensions/`, () => {
|
||||
const content = readSkillFile(file);
|
||||
|
||||
// ~/.gsd/agent/extensions/ should only appear in context that clearly marks
|
||||
// it as reserved/bundled, never as an install target for community extensions.
|
||||
// We check that it does NOT appear as a "Global extensions:" or "Global:" path directive.
|
||||
const lines = content.split("\n");
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (line.includes("~/.gsd/agent/extensions/")) {
|
||||
// If the line references ~/.gsd/agent/extensions/, it must be in a
|
||||
// context explaining it is reserved/bundled — not as an install instruction.
|
||||
const context = lines.slice(Math.max(0, i - 2), i + 3).join("\n");
|
||||
const isBundledContext =
|
||||
context.toLowerCase().includes("bundled") ||
|
||||
context.toLowerCase().includes("reserved") ||
|
||||
context.toLowerCase().includes("synced");
|
||||
assert.ok(
|
||||
isBundledContext,
|
||||
`${label} line ${i + 1} references ~/.gsd/agent/extensions/ without ` +
|
||||
`marking it as bundled/reserved. Context:\n${context}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue