Skip to content

Latest commit

 

History

History
42 lines (32 loc) · 11.4 KB

File metadata and controls

42 lines (32 loc) · 11.4 KB

MEMORY.md

Discovered Knowledge

  • Bun workspace symlinks (v1.3.x): bun install only creates node_modules/@agent-plugin-builder/* symlinks at the repo root when the root package.json declares the workspace packages as dependencies ("workspace:*"). Without root-level references, bun links workspace packages only in the nested node_modules of packages that depend on them — so code at the repo root (e.g. tests/) cannot resolve @agent-plugin-builder/* and fails with Cannot find module. Fix: list all workspace packages as root devDependencies with "workspace:*".

Gotchas

  • CI (GitHub Actions) runs bun install --frozen-lockfile; any fix to module resolution must work from a completely fresh node_modules state, not rely on pre-existing symlinks.
  • The main/types fields already point at ./src/index.ts in all four packages; those were not the cause of the CI failure.
  • Hardcoded /tmp/ in tests fails on the Windows CI runner: the CI matrix (ci.yml) runs [ubuntu-latest, macos-latest, windows-latest]. On Windows, /tmp/x resolves to <drive>:\tmp\x (drive root, e.g. D:\tmp) which does not exist → fs.writeFileSync/non-recursive fs.mkdirSync throw ENOENT, and error-message assertions containing the literal /tmp/... string mismatch the resolved path. Tests that use fs.mkdirSync(dir, { recursive: true }) or fs.mkdtempSync(...) pass on Windows because they create the parent. Fix pattern: use path.join(os.tmpdir(), ...) (equals /tmp on Linux/macOS, real %TEMP% on Windows). When fixing CI-only test failures, always check .github/workflows/ci.yml matrix OSes first.
    • fs.mkdtempSync does NOT create parents (vs mkdirSync recursive) — tests/cli/commands.test.ts (v0.0.9): the module-level fs.mkdtempSync(path.join('/tmp', 'agent-plugins-cli-')) crashed the whole CLI test file at load on Windows (ENOENT: mkdtemp '\tmp\agent-plugins-cli-XXXXXX' → "Unhandled error between tests", 1 error + 1 fail). mkdirSync(recursive:true) creates the <drive>:\tmp parent so other files' /tmp/ fixtures passed; mkdtempSync requires the template dir to exist. Fix: fs.mkdtempSync(path.join(os.tmpdir(), ...)) and replace all 18 hardcoded /tmp/test-cli-* scratch dirs in that file with path.join(os.tmpdir(), ...).

Discovered Knowledge

  • npm publishing (packages/npm): The publishable agent-plugin-builder package bundles the whole CLI (114 modules incl. prompts, commander, chalk) into one file via bun build ../cli/src/index.ts --outdir ./dist --target node. No --external needed — bun bundles CJS deps like prompts fine for the node target. Two traps solved:
    • bun build --target node emits ESM (export { run }), so packages/npm/package.json MUST have "type": "module" or node dies on Unexpected token 'export'.
    • The CLI entry only exports run() (never auto-invokes), so the bin shim must call it: import('../dist/index.js').then(({ run }) => run()). A bare import(...) does nothing.
  • Version coupling: packages/cli/src/index.ts hardcodes the CLI version for --version; the npm bundle is built from that source, so the version lives in two places (packages/npm/package.json + CLI source) and must be bumped in lockstep. Both are 0.0.2.
  • CI publish auth (current): The release job uses NPM_TOKEN via actions/setup-node. OIDC trusted publishing was tried and failed with ENEEDAUTH, so it was reverted. Current shape: permissions: contents: read (no id-token: write), actions/setup-node@v4 with node-version: '20' + registry-url: 'https://registry.npmjs.org' (this is what writes the .npmrc auth line), then npm publish --access public with env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}. No --provenance (that needs OIDC). Do not hand-create packages/npm/.npmrc — setup-node handles it; only setup-bun alone would have needed one.
    • Prerequisite: the NPM_TOKEN repo secret must exist (Automation-type token from npmjs.com). As of the switch, gh api repos/HiAi-gg/agent-plugin-builder/actions/secrets returned no secrets — the token still has to be added or the release job will fail again.

Discovered Knowledge

  • Declarative config (Phases 2–8, v0.0.2): packages/core/src/config.ts adds a plugin.yml schema (pluginConfigSchema) + parseConfigFile/configToPortablePlugin. Config file name can be plugin.yml or agent-plugin.yml — it is passed via create --config <file>.
  • MCP server names: PortableMcpServer carries an optional _name field (underscore-prefixed = pipeline-internal, never serialized). The generator strips _name via const { _name, ...serverData } = server before writing mcp.json (the strict mcp schema would reject it). When _name is absent, the name is derived from the command basename or URL hostname (deriveServerName).
  • Commander --version trap: program.version('0.0.2') registers a program-level --version option that greedily intercepts create --version <version> BEFORE subcommand dispatch (commander's parseOptions consumes options after operands on the parent). Fix: don't use .version(); handle top-level --version/-V manually in run() (args.length === 1 check) so the subcommand flag is untouched.
  • Commander variadic options emit one value per event: with --mcp-args <args...>, commander emits each value as a separate string, so the collector must be (value, previous) => [...previous, value] — spreading value (a string) splits it into characters.
  • LICENSE generation: packages/generator/src/licenses.ts ships full text for MIT, Apache-2.0, ISC, BSD-2-Clause, BSD-3-Clause; unknown SPDX ids get a short notice referencing the id. README/LICENSE are emitted when PortablePlugin._generateReadme / _licenseType are set (or via GeneratePluginOptions.generateReadme/licenseType).

Discovered Knowledge

  • init wizard (v0.0.2): init builds a PortablePlugin via exported helpers (buildPortablePlugin, defaultAnswers, parseCommaSeparated, parseEnvPairs, defaultSkillBody) and calls generatePlugin() — the same API as create. Modes: --config <file> (parseConfigFile/configToPortablePlugin, no prompts), --yes/--non-interactive/CI (defaultAnswers: 1 example skill + README + LICENSE), else interactive prompts wizard with preview via generatePlugin({ dryRun: true }). optsWithGlobals() on the action's command param is the way to read program-level flags (--force, --non-interactive) from a subcommand — subcommand-level duplicate options are shadowed by the program's own option during the parse walk, so a sub-defined --non-interactive never lands in options. Plugin name validated with NAME_PATTERN from @agent-plugin-builder/core (spec/v1); skill names use the stricter ^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$ (they become directory names).
  • init --dry-run (Phase 2.4): init reads the program-level --dry-run global (via optsWithGlobals()) and passes it to generatePlugin on both write paths — the --config declarative path AND the final wizard/--yes path. Dry-run prints Dry run - would create: with relative file paths instead of ✓ Created. Regression tests in tests/cli/commands.test.ts assert no directory/files are created for both paths.
  • Interactive PTY testing: prompts needs a TTY; printf '...' | script -qec "bun ..." /dev/null works, but all input must be sent with printf '%b' (so \r is interpreted) and small sleeps between lines — an instant pipe burst concatenates everything into the first field (each char is a keypress; Enter-as-submit is lost in a burst).
  • Claude/Cursor MCP migration (ECO-012): packages/sources/src/claude/index.ts and packages/sources/src/cursor/index.ts no longer blind-spread .mcp.json server entries. They use an explicit type-mapping block: no type + command present → stdio; stdio/local → stdio; http/streamable-http/remote → streamable-http; sse → sse; unknown type, stdio-missing-command, or http-missing-url → warning + skip. Only known fields (command, args, env, cwd, url, headers) are extracted into PortableMcpServer with _name.
  • Prettier is NOT enforced in this repo: no .prettierrc, prettier not in devDependencies, and the codebase uses single quotes (prettier defaults = double quotes) — prettier --check . fails on ~50 files including untouched ones. The real gates (AGENTS.md) are bun run lint (ESLint), bun run typecheck, bun run test. Match repo style (single quotes, semicolons) rather than running prettier --write (which whole-file reformats to double quotes).
  • MCP cwd normalization (ECO-004, Issue 3): the mcp.json schema (packages/core/src/schemas/mcp.ts) only accepts cwd values starting with ./, ${PLUGIN_ROOT}, or ${PLUGIN_DATA} — it must NOT be relaxed (spec-faithful). Shared helper normalizeMcpCwd() lives in packages/core/src/normalize-cwd.ts (moved there from packages/sources — core cannot import from sources, and both need it): absolute cwd under the base dir → ./<relative>; absolute cwd outside → warning + preserve original; already-valid values preserved. Callers: vscode + opencode adapters (base = project root), and configToPortablePlugin() in packages/core/src/config.ts (base = config dir, label "config directory", warnings appended to migrationWarnings). packages/cli/src/commands/package.ts downgrades cwd schema issues to warnings so hand-written/pre-existing mcp.json files don't block packaging. Note: on Linux, Windows-style absolute cwds (C:\...) are not path.isAbsolute → preserved untouched (can't be meaningfully converted cross-platform).
  • Skill frontmatter parsing (ECO-001 + HR preservation): all five source adapters (claude/cursor/opencode/vscode/codex-adjacent) now use the shared parseSkillFrontmatter() (packages/sources/src/parse-frontmatter.ts) instead of the old toggle loop. Only a --- block at the very start is frontmatter; the FIRST closing --- ends it, and any later --- lines (horizontal rules) stay in the body — previously the toggle loop re-entered "in frontmatter" at a body HR and swallowed everything after it. Generated SKILL.md must contain exactly ONE frontmatter block (2 --- delimiters); a migrated body may legitimately contain more (HRs).
  • Codex TOML args parsing (Issue 1): packages/sources/src/codex/index.ts parses config.toml by splitting on table-header lines ([mcp_servers.<name>]) instead of the old regex that terminated the args match at the first [. parseTomlStringArray() handles quoted strings, escapes, and brackets inside values (e.g. --format=[json]). Unparseable args → warning + omit rather than silent drop.
  • Commander global-flag shadowing (--force on migrate): subcommand-local options that duplicate program-level globals get SHADOWED — options.force in the action handler is undefined while optsWithGlobals().force is true. migrate.ts was reading options.force (broken, silently ignored --force); fixed to opts.force (matches init.ts). If a subcommand's --force/--dry-run ever appears dead, read it via command.optsWithGlobals().
  • create --config warning surfacing: create.ts config path now prints result.warnings (from generatePlugin), so out-of-base cwd warnings from configToPortablePlugin() are visible instead of being silently swallowed. migrationWarnings also live on the PortablePlugin model; plugin.json itself is metadata-only and never serializes them.