Skip to content

Commit 08a4700

Browse files
committed
feat(fleet): add Kimi Code CLI support
1 parent c06d0ca commit 08a4700

10 files changed

Lines changed: 507 additions & 19 deletions

File tree

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,11 @@ yarn-error.log*
8585
!/.claude/settings.json
8686
!/.claude/skills/
8787

88+
# ============================================================================
89+
# Kimi Code CLI user-local overrides
90+
# ============================================================================
91+
/.kimi-code/local.toml
92+
8893
# ============================================================================
8994
# Backup and temporary files
9095
# ============================================================================

.kimi-code/mcp.json

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"mcpServers": {
3+
"chrome-devtools": {
4+
"args": [
5+
"exec",
6+
"chrome-devtools-mcp",
7+
"--isolated",
8+
"--category-extensions",
9+
"--no-category-performance",
10+
"--no-category-emulation"
11+
],
12+
"command": "pnpm"
13+
},
14+
"fff": {
15+
"args": [],
16+
"command": "fff-mcp"
17+
},
18+
"janus-multi": {
19+
"args": ["scripts/fleet/janus-multi-mcp.mts"],
20+
"command": "node"
21+
},
22+
"linear": {
23+
"auth": "oauth",
24+
"type": "http",
25+
"url": "https://mcp.linear.app/mcp"
26+
},
27+
"notion": {
28+
"auth": "oauth",
29+
"type": "http",
30+
"url": "https://mcp.notion.com/mcp"
31+
},
32+
"refero": {
33+
"auth": "oauth",
34+
"type": "http",
35+
"url": "https://api.refero.design/mcp"
36+
}
37+
}
38+
}

bootstrap/fleet.mjs

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1248,6 +1248,7 @@ function parseArgs(argv) {
12481248
repo: DEFAULT_REPO,
12491249
status: false,
12501250
thin: false,
1251+
update: false,
12511252
wire: false,
12521253
}
12531254
for (let i = 0, { length } = argv; i < length; i += 1) {
@@ -1266,6 +1267,7 @@ function parseArgs(argv) {
12661267
else if (arg === '--repo') opts.repo = argv[++i] ?? DEFAULT_REPO
12671268
else if (arg === '--status') opts.status = true
12681269
else if (arg === '--thin') opts.thin = true
1270+
else if (arg === '--update') opts.update = true
12691271
else if (arg === '--wire') opts.wire = true
12701272
}
12711273
return opts
@@ -1474,6 +1476,9 @@ async function installFleet(options) {
14741476
logger.log(
14751477
`install-fleet: placed ${fileCount} file(s) + ${segmentCount} segment(s)${prunedNote} from ${sourceRef} (template ${manifest.templateSha}) → ${dest}.`,
14761478
)
1479+
if (!opts.dryRun) {
1480+
runPostInstallSetup(dest)
1481+
}
14771482
return 0
14781483
} finally {
14791484
rmSync(tmp, {
@@ -1482,6 +1487,52 @@ async function installFleet(options) {
14821487
})
14831488
}
14841489
}
1490+
/**
1491+
* Run member-side setup steps after a fleet bundle is applied. Fail-open: a
1492+
* setup step failure must not roll back a successful bundle install.
1493+
*/
1494+
function runPostInstallSetup(dest) {
1495+
const kimiSetup = path.join(dest, 'scripts', 'fleet', 'setup', 'setup-kimi-user-config.mts')
1496+
if (!existsSync(kimiSetup)) {
1497+
logger.log('install-fleet: setup-kimi-user-config.mts not present — skipping post-install setup.')
1498+
return
1499+
}
1500+
try {
1501+
execFileSync(process.execPath, [kimiSetup], { cwd: dest, stdio: 'pipe' })
1502+
logger.log('install-fleet: Kimi user config synced.')
1503+
} catch (error) {
1504+
logger.warn(
1505+
`install-fleet: Kimi user config setup failed (non-fatal): ${errorMessage(error)}`,
1506+
)
1507+
}
1508+
}
1509+
/**
1510+
* Auto-update mode: apply the newest fleet release if one exists and lock-step
1511+
* allows. Fail-open on network/gh errors so `pnpm install` is never blocked.
1512+
*/
1513+
async function runUpdate(options) {
1514+
const opts = { __proto__: null, ...options }
1515+
const dest = path.resolve(opts.dest ?? repoRoot)
1516+
const repo = opts.repo ?? DEFAULT_REPO
1517+
const cfg = readBundleConfig(dest)
1518+
const currentRef = cfg.ref || ''
1519+
if (!currentRef) {
1520+
if (!opts.quiet)
1521+
logger.log('fleet:update: no bundle.ref pinned — not a thin consumer, nothing to update.')
1522+
return 0
1523+
}
1524+
const newestRef = resolveNewestRef(repo)
1525+
if (!newestRef) {
1526+
if (!opts.quiet) logger.log('fleet:update: could not resolve newest release (offline or no releases).')
1527+
return 0
1528+
}
1529+
if (newestRef === currentRef) {
1530+
if (!opts.quiet) logger.log(`fleet:update: ${currentRef} is already the newest release.`)
1531+
return 0
1532+
}
1533+
logger.log(`fleet:update: ${newestRef} is newer than pinned ${currentRef} — applying…`)
1534+
return installFleet({ ...opts, ref: newestRef })
1535+
}
14851536
function isMainModule() {
14861537
const entry = process.argv[1]
14871538
if (!entry) return false
@@ -1493,9 +1544,13 @@ function isMainModule() {
14931544
}
14941545
if (isMainModule()) {
14951546
const parsed = parseArgs(process.argv.slice(2))
1496-
process.exitCode = parsed.status
1497-
? runStatus(parsed)
1498-
: await installFleet(parsed)
1547+
if (parsed.status) {
1548+
process.exitCode = runStatus(parsed)
1549+
} else if (parsed.update) {
1550+
process.exitCode = await runUpdate(parsed)
1551+
} else {
1552+
process.exitCode = await installFleet(parsed)
1553+
}
14991554
}
15001555

15011556
//#endregion

docs/agents.md/fleet/cross-tool-agents.md

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
# Cross-tool agents: instructions, skills, memory, detection
22

3-
The fleet's automation is authored for **Claude Code**, but Codex and OpenCode
4-
(and Gemini) read some of the same surfaces. This doc records what ports across
5-
tools, what doesn't, and the code that makes the fleet agent- + platform-aware.
3+
The fleet's automation is authored for **Claude Code**, but Codex, OpenCode,
4+
and Kimi Code CLI read some of the same surfaces. This doc records what ports
5+
across tools, what doesn't, and the code that makes the fleet agent- +
6+
platform-aware.
67

7-
## The three surfaces, by portability
8+
## The surfaces, by portability
89

9-
| Surface | Claude Code | Codex CLI | OpenCode | Ports? |
10-
| -------------------------- | ----------------------------------- | ----------------------------- | ------------------------------------------------- | ---------------------------------------------------- |
11-
| **Instructions** | `CLAUDE.md` | `AGENTS.md` | `AGENTS.md` | yes, via the `AGENTS.md → CLAUDE.md` symlink |
12-
| **Skills** | `.claude/skills/<name>/SKILL.md` | `.agents/skills/` (one level) | `.claude/skills/` + `.agents/skills/` (one level) | yes, via the generated `.agents/skills/` flat mirror |
13-
| **Commands** | `.claude/commands/` | Codex slash-commands | OpenCode commands | no (per-tool format) |
14-
| **Hooks** | `.claude/hooks/` (stdin JSON) | Codex Hooks | OpenCode plugins (event callbacks) | no (per-tool mechanism) |
15-
| **Memory** (agent-written) | `~/.claude/projects/<slug>/memory/` | none | none | n/a (only Claude has it) |
10+
| Surface | Claude Code | Codex CLI | OpenCode | Kimi Code CLI | Ports? |
11+
| -------------------------- | ----------------------------------- | ----------------------------- | ------------------------------------------------- | -------------------------------------- | ---------------------------------------------------- |
12+
| **Instructions** | `CLAUDE.md` | `AGENTS.md` | `AGENTS.md` | none (user config only) | yes for Codex/OpenCode via `AGENTS.md → CLAUDE.md` |
13+
| **Skills** | `.claude/skills/<name>/SKILL.md` | `.agents/skills/` (one level) | `.claude/skills/` + `.agents/skills/` (one level) | none | yes for Codex/OpenCode via `.agents/skills/` mirror |
14+
| **Commands** | `.claude/commands/` | Codex slash-commands | OpenCode commands | none | no (per-tool format) |
15+
| **Hooks** | `.claude/hooks/` (stdin JSON) | Codex Hooks | OpenCode plugins (event callbacks) | none | no (per-tool mechanism) |
16+
| **MCP servers** | `.mcp.json` | `.codex/config.toml` | `opencode.json` | `.kimi-code/mcp.json` | yes, generated from `.mcp.json` |
17+
| **Permissions** | `.claude/settings.json` | Codex config | OpenCode config | `~/.kimi-code/config.toml` | no (per-tool config) |
18+
| **Memory** (agent-written) | `~/.claude/projects/<slug>/memory/` | none | none | none | n/a (only Claude has it) |
1619

1720
## Instructions — `AGENTS.md → CLAUDE.md`
1821

@@ -58,6 +61,30 @@ self-written memory**: each session starts fresh from the human-authored
5861
(via the CLAUDE.md symlink). When a durable Claude memory is worth sharing across
5962
tools, codify it into CLAUDE.md and every tool sees it through AGENTS.md.
6063

64+
## Kimi Code CLI — generated MCP + user config
65+
66+
Kimi Code CLI has no project-level instructions file; it reads user-owned config
67+
from `~/.kimi-code/config.toml` and project-local MCP servers from
68+
`.kimi-code/mcp.json`. The fleet treats `.mcp.json` as the single committed
69+
authority and generates the Kimi adapter from it.
70+
71+
- **`.kimi-code/mcp.json`** is produced by `scripts/fleet/mcp-config.mts`
72+
(`pnpm run setup:mcp` regenerates it). It carries the same stdio commands and
73+
HTTP OAuth servers as `.codex/config.toml` and `opencode.json`, but in Kimi's
74+
native JSON shape.
75+
- **`~/.kimi-code/config.toml`** is managed by
76+
`scripts/fleet/setup/setup-kimi-user-config.mts`
77+
(`pnpm run setup:kimi-user-config`). It extracts the fleet-canonical
78+
`permissions` block from `.claude/settings.json` and rewrites it as Kimi
79+
`[[permission.rules]]` entries, preserving any user-owned rules outside the
80+
fleet block.
81+
- **`.kimi-code/local.toml`** is ignored by git so operators can keep local
82+
overrides without dirtying the tree.
83+
84+
Kimi ships in the release bundle as generated files only (no symlinks). The
85+
bootstrap installer (`bootstrap/fleet.mjs --update`) and `pnpm install` prepare
86+
step keep the project-local `.kimi-code/mcp.json` and user config current.
87+
6188
## Detection + paths — `@socketsecurity/lib/ai/agent-context`
6289

6390
Hooks receive **no agent id in their stdin payload**; the running agent is

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@
4545
"// Setup": "",
4646
"setup": "node scripts/setup.mts",
4747
"postinstall": "node scripts/setup.mts --install --quiet",
48-
"prepare": "node scripts/fleet/install-git-hooks.mts",
48+
"prepare": "node scripts/fleet/prepare.mts",
49+
"fleet:update": "node bootstrap/fleet.mjs --update",
50+
"setup:kimi-user-config": "node scripts/fleet/setup/setup-kimi-user-config.mts",
4951
"pretest": "pnpm run build:cli",
5052
"setup-security-tools": "node .claude/hooks/fleet/setup-security-tools/install.mts",
5153
"ci:local": "node scripts/fleet/agent-ci-skip-locks.mts run --all --quiet --pause-on-failure --github-token",

scripts/fleet/check/mcp-client-configs-are-current.mts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { isMainModule } from '../_shared/is-main-module.mts'
1616
import {
1717
parseCanonicalMcpConfig,
1818
renderCodexMcpConfig,
19+
renderKimiProjectMcpConfig,
1920
renderOpenCodeMcpConfig,
2021
} from '../mcp-config.mts'
2122
import type { PortableMcpServers } from '../mcp-config.mts'
@@ -27,7 +28,7 @@ function main(): void {
2728
const issues = findMcpClientConfigIssues(REPO_ROOT)
2829
if (issues.length === 0) {
2930
logger.success(
30-
'[mcp-client-configs-are-current] Codex/OpenCode configs match .mcp.json.',
31+
'[mcp-client-configs-are-current] Codex/OpenCode/Kimi configs match .mcp.json.',
3132
)
3233
return
3334
}
@@ -70,6 +71,10 @@ export function findMcpClientConfigIssues(repoRoot: string): string[] {
7071
content: renderOpenCodeMcpConfig(servers),
7172
relativePath: 'opencode.json',
7273
},
74+
{
75+
content: renderKimiProjectMcpConfig(servers),
76+
relativePath: '.kimi-code/mcp.json',
77+
},
7378
]
7479
const issues: string[] = []
7580
for (let i = 0, { length } = expected; i < length; i += 1) {

scripts/fleet/mcp-config.mts

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* @file Pure adapters from the fleet-canonical Claude `.mcp.json` shape to
3-
* project-local Codex/OpenCode configs and Kimi's per-user MCP file.
3+
* project-local Codex/OpenCode/Kimi configs and Kimi's per-user MCP file.
44
* Credentials never belong in the canonical or generated project files; each
55
* client owns OAuth state in its user data directory.
66
*/
@@ -62,6 +62,34 @@ function assertNoCredentials(value: unknown, location = '.mcp.json'): void {
6262
}
6363
}
6464

65+
function compactKimiArgsArrays(
66+
text: string,
67+
servers: PortableMcpServers,
68+
): string {
69+
let result = text
70+
const items = Object.values(servers)
71+
for (let i = 0, { length } = items; i < length; i += 1) {
72+
const server = items[i]!
73+
if (server.kind !== 'stdio' || server.args.length === 0) {
74+
continue
75+
}
76+
const compact = ` "args": [${server.args.map(value => JSON.stringify(value)).join(', ')}]`
77+
if (compact.length > 80) {
78+
continue
79+
}
80+
const expanded = [
81+
' "args": [',
82+
...server.args.map(
83+
(value, index) =>
84+
` ${JSON.stringify(value)}${index + 1 < server.args.length ? ',' : ''}`,
85+
),
86+
' ]',
87+
].join('\n')
88+
result = result.replace(expanded, compact)
89+
}
90+
return result
91+
}
92+
6593
function compactOpenCodeCommandArrays(
6694
text: string,
6795
servers: PortableMcpServers,
@@ -102,7 +130,9 @@ function main(): void {
102130
)
103131
}
104132
writeMcpClientConfigs(REPO_ROOT)
105-
process.stdout.write('Generated .codex/config.toml and opencode.json.\n')
133+
process.stdout.write(
134+
'Generated .codex/config.toml, opencode.json, and .kimi-code/mcp.json.\n',
135+
)
106136
}
107137

108138
function parseStringArray(value: unknown, field: string): string[] {
@@ -268,7 +298,28 @@ export function renderOpenCodeMcpConfig(servers: PortableMcpServers): string {
268298
}
269299

270300
/**
271-
* Regenerate the two committed project adapters from `.mcp.json`.
301+
* Render Kimi's project-local `~/.kimi-code/mcp.json` adapter. Kimi resolves
302+
* stdio commands relative to the project root when the file lives at
303+
* `<project>/.kimi-code/mcp.json`, so no `cwd` is needed.
304+
*/
305+
export function renderKimiProjectMcpConfig(servers: PortableMcpServers): string {
306+
const mcpServers: Record<string, unknown> = {}
307+
for (const [name, server] of Object.entries(sortRecord(servers))) {
308+
mcpServers[name] =
309+
server.kind === 'http'
310+
? { auth: 'oauth', type: 'http', url: server.url }
311+
: { args: server.args, command: server.command }
312+
}
313+
const rendered = JSON.stringify(
314+
{ mcpServers: sortRecord(mcpServers) },
315+
undefined,
316+
2,
317+
)
318+
return `${compactKimiArgsArrays(rendered, servers)}\n`
319+
}
320+
321+
/**
322+
* Regenerate the three committed project adapters from `.mcp.json`.
272323
*/
273324
export function writeMcpClientConfigs(repoRoot: string): void {
274325
const templateRoot = path.join(repoRoot, 'template', 'base')
@@ -279,6 +330,7 @@ export function writeMcpClientConfigs(repoRoot: string): void {
279330
readFileSync(path.join(configRoot, '.mcp.json'), 'utf8'),
280331
)
281332
mkdirSync(path.join(configRoot, '.codex'), { recursive: true })
333+
mkdirSync(path.join(configRoot, '.kimi-code'), { recursive: true })
282334
writeFileSync(
283335
path.join(configRoot, '.codex', 'config.toml'),
284336
renderCodexMcpConfig(servers),
@@ -287,6 +339,10 @@ export function writeMcpClientConfigs(repoRoot: string): void {
287339
path.join(configRoot, 'opencode.json'),
288340
renderOpenCodeMcpConfig(servers),
289341
)
342+
writeFileSync(
343+
path.join(configRoot, '.kimi-code', 'mcp.json'),
344+
renderKimiProjectMcpConfig(servers),
345+
)
290346
}
291347

292348
if (isMainModule(import.meta.url)) {

0 commit comments

Comments
 (0)