Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ jobs:
contents: write
strategy:
fail-fast: false
# Four marketplace plus five flat β€” opencode is flat-only. Mirrors the CLI golden
# Four marketplace plus six flat β€” opencode and kilo are flat-only. Mirrors the CLI golden
# snapshot matrix.
matrix:
include:
Expand All @@ -147,6 +147,7 @@ jobs:
- { tool: copilot, mode: flat }
- { tool: codex, mode: flat }
- { tool: opencode, mode: flat }
- { tool: kilo, mode: flat }
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

Expand Down
28 changes: 28 additions & 0 deletions .github/workflows/cli-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,32 @@ jobs:
# token and no network. `smoke:full` adds the remote-fetch section on demand.
- run: cd cli && pnpm smoke

cli-kilo-runtime:
name: cli / Kilo runtime smoke
needs: [changes]
if: needs.changes.outputs.relevant == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install pnpm
run: corepack enable
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "22"
- name: Cache pnpm store
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.local/share/pnpm/store
key: pnpm-${{ runner.os }}-${{ hashFiles('cli/pnpm-lock.yaml') }}
restore-keys: pnpm-${{ runner.os }}-
- run: cd cli && pnpm install --frozen-lockfile
# The test starts Kilo's local server and asks it to load the generated project plugin.
# No model or account is needed for this protocol-level smoke.
- name: Install Kilo Code CLI
run: npm install -g @kilocode/cli@7.7.5
- run: cd cli && pnpm test:e2e:kilo

cli-build:
name: cli / Build & Bundle Budget
needs: [changes]
Expand Down Expand Up @@ -591,6 +617,7 @@ jobs:
- cli-architecture
- cli-coverage
- cli-smoke
- cli-kilo-runtime
- cli-build
- cli-knip
- identifier-join
Expand All @@ -611,6 +638,7 @@ jobs:
"${{ needs.cli-architecture.result }}" \
"${{ needs.cli-coverage.result }}" \
"${{ needs.cli-smoke.result }}" \
"${{ needs.cli-kilo-runtime.result }}" \
"${{ needs.cli-build.result }}" \
"${{ needs.cli-knip.result }}" \
"${{ needs.identifier-join.result }}" \
Expand Down
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Why not just write your own commands? β†’ [FAQ](docs/FAQ.md#-why-aidd-instead-of

## βœ… Prerequisites

- **An AI coding tool** β€” Claude Code (native), or Cursor / Copilot / Codex / OpenCode (see [Compatibility](#-compatibility)).
- **An AI coding tool** β€” Claude Code (native), or Cursor / Copilot / Codex / OpenCode / Kilo Code (see [Compatibility](#-compatibility)).
- **[Node](https://nodejs.org) 22 or later** on your `PATH`, only for the plugin that ships hooks ([what they do](docs/ARCHITECTURE.md#-bundled-hooks)); the workflows themselves are markdown and need nothing.

## πŸ”Œ Compatibility
Expand All @@ -49,6 +49,7 @@ Why not just write your own commands? β†’ [FAQ](docs/FAQ.md#-why-aidd-instead-of
| **GitHub Copilot** | βœ… Supported | Marketplace Β· Flat |
| **Codex** | βœ… Supported | Marketplace Β· Flat |
| **OpenCode** | βœ… Supported | Flat |
| **Kilo Code** | βœ… Supported | Flat |
| **Gemini Β· Mistral** | 🚧 In progress | β€” |

<sub>**Marketplace** = installed and updated through your tool's plugin manager. **Flat** = files copied directly into your project, no plugin manager involved. Install steps per tool β†’ [Other tools](#other-tools).</sub>
Expand Down Expand Up @@ -174,6 +175,16 @@ codex plugin add aidd-context@aidd-framework # per plugin

</details>

<details>
<summary><strong>Kilo Code</strong> β€” Flat only</summary>

1. Unzip the `kilo-flat` archive into your project root β†’ `.kilo/`, including `.kilo/kilo.jsonc`.
2. Start a new Kilo session so it loads the generated project plugin.

[Plugins documentation](https://kilo.ai/docs/automate/extending/plugins)

</details>

## πŸš€ Quick start

Three ways in β€” pick one:
Expand Down
6 changes: 4 additions & 2 deletions cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
`@ai-driven-dev/cli` installs AI tool runtime configs, IDE integrations, and plugins from an AIDD marketplace into a project.
Every file it writes is hash-tracked in a manifest, so drift is detected and owned files can be restored.

Supported AI tools: Claude Code, Cursor, GitHub Copilot, Codex, OpenCode. Supported IDE: VS Code.
Supported AI tools: Claude Code, Cursor, GitHub Copilot, Codex, OpenCode, Kilo Code. Supported IDE: VS Code.
Requires Node.js >= 22.12, and `git` to fetch marketplace plugins.

## Install
Expand Down Expand Up @@ -136,8 +136,9 @@ Two output layouts, chosen by `--as`:
| `copilot` | yes | yes | `.github/` |
| `codex` | yes | yes | `.codex/` |
| `opencode` | no | yes | `.opencode/` |
| `kilo` | no | yes | `.kilo/` |

OpenCode declares no marketplace contract, so it is flat only. Every other target accepts both layouts.
OpenCode and Kilo Code declare no marketplace contract, so they are flat only. Every other target accepts both layouts.

## Environment variables

Expand Down Expand Up @@ -184,6 +185,7 @@ Per tool, the settings file the CLI writes:
| GitHub Copilot | Plugin recommendations in `.github/copilot/settings.json`, MCP servers in `.vscode/mcp.json`, plus `.vscode/settings.json` when the VS Code tool is installed too |
| Codex | `.codex/config.toml` |
| OpenCode | `opencode.json`, or `opencode.jsonc` when that is the one present |
| Kilo Code | `.kilo/kilo.jsonc` by default; an existing `kilo.json[c]` is reused |
| VS Code | `.vscode/settings.json`, `.vscode/extensions.json`, `.vscode/keybindings.json` |

## More
Expand Down
1 change: 1 addition & 0 deletions cli/aidd_docs/memory/codebase-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ src/
β”‚ β”‚ β”‚ β”œβ”€β”€ codex/
β”‚ β”‚ β”‚ β”œβ”€β”€ copilot/
β”‚ β”‚ β”‚ β”œβ”€β”€ cursor/
β”‚ β”‚ β”‚ β”œβ”€β”€ kilo/
β”‚ β”‚ β”‚ β”œβ”€β”€ opencode/
β”‚ β”‚ β”‚ └── vscode/
β”‚ β”‚ └── infrastructure/
Expand Down
3 changes: 3 additions & 0 deletions cli/assets/configs/kilo/kilo.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"$schema": "https://app.kilo.ai/config.json"
}
5 changes: 5 additions & 0 deletions cli/mutation-scopes.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"!src/contexts/tools/domain/profiles/codex/**/*.ts",
"!src/contexts/tools/domain/profiles/copilot/**/*.ts",
"!src/contexts/tools/domain/profiles/cursor/**/*.ts",
"!src/contexts/tools/domain/profiles/kilo/**/*.ts",
"!src/contexts/tools/domain/profiles/opencode/**/*.ts"
],
"break": 94
Expand Down Expand Up @@ -60,6 +61,10 @@
"mutate": "src/contexts/tools/domain/profiles/opencode/**/*.ts",
"break": 94
},
"tools-kilo": {
"mutate": "src/contexts/tools/domain/profiles/kilo/**/*.ts",
"break": 64
},
"canary": {
"mutate": [
"tests/fixtures/stryker-canary/**/*.ts",
Expand Down
6 changes: 4 additions & 2 deletions cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"node": ">=22.12"
},
"packageManager": "pnpm@12.3.4",
"bundleBudgetKB": 722,
"bundleBudgetKB": 734,
"scripts": {
"build": "tsup && node scripts/check-bundle-size.mjs",
"build:check-size": "node scripts/check-bundle-size.mjs",
Expand All @@ -45,7 +45,8 @@
"test:arch": "vitest run --project=architecture",
"test:unit": "vitest run --project=unit",
"test:integration": "vitest run --project=integration",
"test:e2e": "vitest run --project=e2e",
"test:e2e": "vitest run --project=e2e",
"test:e2e:kilo": "KILO_RUNTIME_SMOKE=1 vitest run --project=e2e tests/e2e/kilo-runtime.e2e.test.ts",
"test:coverage": "vitest run --coverage",
"test:kanban": "pnpm --dir ../kanban test",
"test:watch": "vitest",
Expand All @@ -66,6 +67,7 @@
"test:mutation:tools-codex": "node scripts/run-mutation.mjs tools-codex",
"test:mutation:tools-copilot": "node scripts/run-mutation.mjs tools-copilot",
"test:mutation:tools-cursor": "node scripts/run-mutation.mjs tools-cursor",
"test:mutation:tools-kilo": "node scripts/run-mutation.mjs tools-kilo",
"test:mutation:tools-opencode": "node scripts/run-mutation.mjs tools-opencode",
"test:mutation:telemetry": "node scripts/run-mutation.mjs telemetry",
"test:mutation:translate": "node scripts/run-mutation.mjs translate",
Expand Down
2 changes: 2 additions & 0 deletions cli/scripts/check-bundle-size.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ const pkg = JSON.parse(readFileSync(resolve(root, "package.json"), "utf8"));
// guards for #829 (+22.0 KB over the first 829 candidate's 681.8); 0.9% measured headroom.
// 722 KB: 714.2 KB, exact native-claim partition, multihost preflight, and symlink boundary
// guards for #829 (+10.4 KB over the preceding 703.8 KB); 1.1% measured headroom.
// 734 KB: 725.8 KB, Kilo Code's profile, generated bridge, and runtime smoke support (+9.6 KB
// over next's 716.2 KB); 1.1% measured headroom.
const budgetKB = pkg.bundleBudgetKB ?? 500;
const budgetBytes = budgetKB * 1024;

Expand Down
16 changes: 9 additions & 7 deletions cli/scripts/smoke-tools.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
CLI="$ROOT/dist/cli.js"
FRAMEWORK_FIXTURE="$ROOT/tests/fixtures/framework"

AI_TOOLS=(claude cursor copilot codex opencode)
AI_TOOLS=(claude cursor copilot codex opencode kilo)
IDE_TOOLS=(vscode)

# Canonical leaf-command surface. Coverage = exercised / total.
Expand Down Expand Up @@ -232,15 +232,17 @@ run "marketplace add --overwrite" 0 "" "$P_MKT" -- marketplace add local "$MKT_S
# Passing `--scope` is not enough: the two values must write to different places.
P_SCOPE=$(new_project)
(cd "$P_SCOPE" && node "$CLI" setup --source local --path "$FRAMEWORK_FIXTURE" --ai claude --plugins none --yes >/dev/null 2>&1)
run "marketplace add --scope project" 0 "" "$P_SCOPE" -- marketplace add scoped "$MKT_SRC" --yes --scope project
PROJECT_MKT_SRC="$TMPROOT/project-mkt-src"; mkdir -p "$PROJECT_MKT_SRC/.claude-plugin"
printf '%s' '{"name":"project-mkt","owner":{"name":"smoke"},"version":"1.0.0","plugins":[]}' > "$PROJECT_MKT_SRC/.claude-plugin/marketplace.json"
run "marketplace add --scope project" 0 "" "$P_SCOPE" -- marketplace add scoped "$PROJECT_MKT_SRC" --yes --scope project
proj_reg="$P_SCOPE/.aidd/marketplaces.json"
if [[ -f "$proj_reg" ]] && grep -q "scoped" "$proj_reg"; then
ok "--scope project writes the project registry"
else
bad "--scope project did not write $proj_reg"
fi
# A second source with its own manifest name: the tool keys its registry by the name inside
# the marketplace, so two aidd marketplaces sharing a source would collide rather than scope.
# A third source with its own manifest name: the tool keys its registry by the name inside
# the marketplace, so each scope assertion needs a distinct host name.
USER_MKT_SRC="$TMPROOT/user-mkt-src"; mkdir -p "$USER_MKT_SRC/.claude-plugin"
printf '%s' '{"name":"user-mkt","owner":{"name":"smoke"},"version":"1.0.0","plugins":[]}' > "$USER_MKT_SRC/.claude-plugin/marketplace.json"
run "marketplace add --scope user" 0 "" "$P_SCOPE" -- marketplace add userscoped "$USER_MKT_SRC" --yes --scope user
Expand All @@ -256,9 +258,9 @@ if command -v claude >/dev/null 2>&1; then
claude_local="$P_SCOPE/.claude/settings.local.json"
claude_home="$HOME/.claude/settings.json"
# Names the marketplace, not the generic `extraKnownMarketplaces` key any declaration would
# satisfy. Keyed by `local-mkt`, the catalog's own declared name: this file is written by
# satisfy. Keyed by `project-mkt`, the catalog's own declared name: this file is written by
# `hostName`, never by `scoped`, aidd's local alias for the same entry.
if [[ -f "$claude_local" ]] && grep -q '"local-mkt"' "$claude_local"; then
if [[ -f "$claude_local" ]] && grep -q '"project-mkt"' "$claude_local"; then
ok "claude declares the project marketplace at local scope"
else
bad "claude has no local-scope declaration in $claude_local"
Expand Down Expand Up @@ -349,7 +351,7 @@ if true; then
run "sync --force" 0 "" "$BASE" -- sync --force
repaired "sync --force" "$tgt"

section "framework install/update/remove --tool Γ— all 5 AI tools + vscode"
section "framework install/update/remove --tool Γ— all 6 AI tools + vscode"
run "framework update (all)" 0 "" "$BASE" -- framework update
run "framework rules" 0 "" "$BASE" -- framework rules
run "framework rules --json" 0 "" "$BASE" -- framework rules --json
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type { AiToolId } from "../../../../../kernel/tool.js";
import type { MarketplaceRegistry } from "../../../../distribution/domain/ports/marketplace-registry.js";
import {
frameworkBuildModeFor,
getAiToolConfig,
resolvePluginsCapability,
} from "../../../../tools/domain/registry.js";
import type { PluginDistribution } from "../../../../translate/domain/plugin-distribution.js";
Expand All @@ -30,6 +31,7 @@ import {
resolveScopeForInstall,
} from "../../plugin/plugin-target-resolution.js";
import type { EnsureBuiltMarketplace } from "../../shared/ensure-built-marketplace-use-case.js";
import { materializeFlatMcp, refreshTrackedMcpConfigHash } from "./flat-mcp-materializer.js";
import { ModeBFlatMaterializationTranslator } from "./mode-b-flat-materialization-translator.js";
import type { PluginTranslator } from "./plugin-translator.js";
import { ProjectHooksMaterializer } from "./project-hooks-materializer.js";
Expand Down Expand Up @@ -99,6 +101,15 @@ export class BuiltTreeMaterializationTranslator implements PluginTranslator {
const files = deliversHooksToProject
? withoutHooksPrefix(builtFiles, dist.manifest.name)
: builtFiles;
const mcp = await materializeFlatMcp(
this.fs,
this.hasher,
dist,
toolId,
projectRoot,
previousMcpEntries
);
await refreshTrackedMcpConfigHash(this.fs, manifest, toolId, projectRoot, mcp.outputRelPath);
const scope = resolveScopeForInstall(toolId);
const baseDir =
mode === "flat"
Expand All @@ -109,11 +120,19 @@ export class BuiltTreeMaterializationTranslator implements PluginTranslator {
manifest.addPlugin(
toolId,
InstalledPlugin.withProjectHooks(
InstalledPlugin.fromDistribution(dist, source, owned, scope, new Map(), marketplace),
InstalledPlugin.fromDistributionWithMcp(
dist,
source,
owned,
mcp.mcpEntries,
scope,
new Map(),
marketplace
),
hooks.projectHooks
)
);
return { skipped: hooks.skipped, written };
return { skipped: [...mcp.mcpSkips, ...hooks.skipped], written };
}

// Skips a file already matching the built content on disk, so a no-op restore reports (and
Expand Down Expand Up @@ -148,7 +167,7 @@ export class BuiltTreeMaterializationTranslator implements PluginTranslator {
const files: InstallationFile[] = [];
for (const abs of absPaths) {
const rel = posixRelative(builtDir, abs);
if (!this.belongsToPlugin(rel, name) && !hookPaths.has(rel)) continue;
if (!this.belongsToPlugin(rel, name, toolId) && !hookPaths.has(rel)) continue;
const content = await this.fs.readFile(abs);
files.push(
new InstallationFile({ relativePath: rel, content, hash: this.hasher.hash(content) })
Expand All @@ -157,9 +176,10 @@ export class BuiltTreeMaterializationTranslator implements PluginTranslator {
return files;
}

private belongsToPlugin(rel: string, name: string): boolean {
private belongsToPlugin(rel: string, name: string, toolId: AiToolId): boolean {
const segments = rel.split("/");
if (segments[0] !== ".opencode" || segments.length < 3) return false;
const toolDirectory = getAiToolConfig(toolId).directory.replace(/\/$/, "");
if (segments[0] !== toolDirectory || segments.length < 3) return false;
// `skills/` nests the whole plugin under one exactly-named segment; every other flat section
// hyphen-prefixes the leaf segment.
if (segments[1] === "skills") return segments[2] === name;
Expand All @@ -171,7 +191,7 @@ export class BuiltTreeMaterializationTranslator implements PluginTranslator {
const flatHooksDir = plugins?.flatHooksDir;
if (plugins === null || flatHooksDir === null || flatHooksDir === undefined) return new Set();
const name = dist.manifest.name;
return new Set(
const paths = new Set(
dist.components.hooks
.filter((f) => f.relativePath !== "hooks/hooks.json")
.map((f) =>
Expand All @@ -183,6 +203,15 @@ export class BuiltTreeMaterializationTranslator implements PluginTranslator {
)
)
);
const bridge = plugins.flatHooksBridge;
const hooksJson = dist.components.hooks.find((f) => f.relativePath === "hooks/hooks.json");
const hasOwnBridge =
bridge !== null &&
dist.components.hooks.some((f) => f.relativePath.endsWith(`/${bridge.skipIfSourceHas}`));
if (bridge !== null && hooksJson !== undefined && !hasOwnBridge) {
if (bridge.generate(hooksJson.content, name) !== null) paths.add(bridge.path(name));
}
return paths;
}

private async findMarketplace(name: string, projectRoot: string) {
Expand Down
Loading
Loading