@kal-elsam/kairo-runtime 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +84 -0
- package/bin/kairo-runtime.js +0 -0
- package/bin/kairo.js +0 -0
- package/package.json +1 -1
- package/src/cli.js +172 -8
- package/src/global/check-resolutions.js +31 -0
- package/src/global/cli-help.js +19 -2
- package/src/global/component-ecosystem-checks.js +2 -0
- package/src/global/component-integration-cli.js +29 -10
- package/src/global/components-resolve-cli.js +246 -0
- package/src/global/connection-actions.js +147 -0
- package/src/global/connections.js +269 -0
- package/src/global/fleet-configure-plan.js +123 -0
- package/src/global/fleet-configure.js +303 -0
- package/src/global/fleet-models.js +188 -0
- package/src/global/fleet-set.js +219 -0
- package/src/global/fleet-shared.js +38 -0
- package/src/global/ink/cockpit-controller.js +1 -1
- package/src/global/ink/cockpit-models.js +4 -1
- package/src/global/ink/orchestrator-app.js +21 -2
- package/src/global/ink/ux/live-overview.js +5 -11
- package/src/global/ink/ux/overview-needs.js +1 -1
- package/src/global/integrations/engram-evidence.js +7 -2
- package/src/global/integrations/sdd-apply.js +17 -7
- package/src/global/integrations/sdd-evidence.js +22 -3
- package/src/global/integrations/sdd-plan.js +21 -3
- package/src/global/integrations/sdd-resolutions.js +73 -0
- package/src/global/integrations/sdd-state.js +69 -0
- package/src/global/integrations/sdd-verify.js +9 -4
- package/src/global/mcp/kairo-mcp.js +56 -5
- package/src/global/mcp/resolve-mcp-workspace.js +51 -0
- package/src/global/mcp/work-snapshot-rule.js +89 -0
- package/src/global/mcp/work-snapshot-tool.js +49 -0
- package/src/global/mcp-install.js +239 -0
- package/src/global/next/next-cli.js +35 -0
- package/src/global/next/next-report.js +145 -0
- package/src/global/next/project-key.js +36 -0
- package/src/global/next/publish-work-snapshot.js +116 -0
- package/src/global/next/work-enroll.js +91 -0
- package/src/global/next/work-snapshot.js +216 -0
- package/src/global/observability/fleet-activity.js +197 -0
- package/src/global/observability/fleet-models-catalog.js +137 -0
- package/src/global/observability/fleet-platforms.js +166 -0
- package/src/global/observability/fleet-probe.js +229 -0
- package/src/global/paths.js +2 -1
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live OpenCode fleet activity from opencode.db (read-only).
|
|
3
|
+
* Parent→child sessions via session.parent_id — not token telemetry.
|
|
4
|
+
*/
|
|
5
|
+
import { access } from "node:fs/promises";
|
|
6
|
+
import { constants as fsConstants } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { resolveHomeDir } from "../paths.js";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_LIMIT = 40;
|
|
11
|
+
const ACTIVE_WINDOW_MS = 15 * 60 * 1000;
|
|
12
|
+
|
|
13
|
+
async function pathExists(path) {
|
|
14
|
+
try {
|
|
15
|
+
await access(path, fsConstants.F_OK);
|
|
16
|
+
return true;
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function resolveOpenCodeDbPath(homeDir = resolveHomeDir()) {
|
|
23
|
+
return join(homeDir, ".local", "share", "opencode", "opencode.db");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function parseSessionModel(raw) {
|
|
27
|
+
if (raw == null || raw === "") return { model: null, modelShort: null, providerId: null };
|
|
28
|
+
if (typeof raw === "object" && !Array.isArray(raw)) {
|
|
29
|
+
const id = typeof raw.id === "string" ? raw.id : null;
|
|
30
|
+
const providerID = typeof raw.providerID === "string" ? raw.providerID : null;
|
|
31
|
+
const model = id && providerID ? `${providerID}/${id}` : id;
|
|
32
|
+
return { model, modelShort: id, providerId: providerID };
|
|
33
|
+
}
|
|
34
|
+
if (typeof raw !== "string") return { model: null, modelShort: null, providerId: null };
|
|
35
|
+
try {
|
|
36
|
+
return parseSessionModel(JSON.parse(raw));
|
|
37
|
+
} catch {
|
|
38
|
+
const slash = raw.lastIndexOf("/");
|
|
39
|
+
return {
|
|
40
|
+
model: raw,
|
|
41
|
+
modelShort: slash >= 0 ? raw.slice(slash + 1) : raw,
|
|
42
|
+
providerId: slash >= 0 ? raw.slice(0, slash) : null
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function sessionState(row, nowMs, activeWindowMs) {
|
|
48
|
+
if (row.time_archived) return "archived";
|
|
49
|
+
if (typeof row.time_updated === "number" && nowMs - row.time_updated <= activeWindowMs) {
|
|
50
|
+
return "active";
|
|
51
|
+
}
|
|
52
|
+
return "idle";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Pure mapper: DB rows → activity tree nodes.
|
|
57
|
+
*/
|
|
58
|
+
export function mapSessionRowsToActivity(rows = [], {
|
|
59
|
+
nowMs = Date.now(),
|
|
60
|
+
activeWindowMs = ACTIVE_WINDOW_MS
|
|
61
|
+
} = {}) {
|
|
62
|
+
const sessions = rows.map((row) => {
|
|
63
|
+
const parsed = parseSessionModel(row.model);
|
|
64
|
+
return {
|
|
65
|
+
id: String(row.id),
|
|
66
|
+
parentId: row.parent_id ? String(row.parent_id) : null,
|
|
67
|
+
title: typeof row.title === "string" ? row.title : "",
|
|
68
|
+
agent: typeof row.agent === "string" ? row.agent : null,
|
|
69
|
+
model: parsed.model,
|
|
70
|
+
modelShort: parsed.modelShort,
|
|
71
|
+
providerId: parsed.providerId,
|
|
72
|
+
timeCreated: row.time_created ?? null,
|
|
73
|
+
timeUpdated: row.time_updated ?? null,
|
|
74
|
+
state: sessionState(row, nowMs, activeWindowMs),
|
|
75
|
+
platform: "opencode"
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const byAgent = new Map();
|
|
80
|
+
for (const s of sessions) {
|
|
81
|
+
const key = s.agent ?? "unknown";
|
|
82
|
+
const prev = byAgent.get(key);
|
|
83
|
+
if (!prev || (s.timeUpdated ?? 0) > (prev.timeUpdated ?? 0)) {
|
|
84
|
+
byAgent.set(key, s);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const agents = [...byAgent.entries()]
|
|
89
|
+
.map(([id, session]) => ({
|
|
90
|
+
id,
|
|
91
|
+
state: session.state === "archived" ? "idle" : session.state,
|
|
92
|
+
model: session.model,
|
|
93
|
+
modelShort: session.modelShort,
|
|
94
|
+
sessionId: session.id,
|
|
95
|
+
parentId: session.parentId,
|
|
96
|
+
title: session.title,
|
|
97
|
+
timeUpdated: session.timeUpdated
|
|
98
|
+
}))
|
|
99
|
+
.sort((a, b) => {
|
|
100
|
+
if (a.state === "active" && b.state !== "active") return -1;
|
|
101
|
+
if (b.state === "active" && a.state !== "active") return 1;
|
|
102
|
+
return (b.timeUpdated ?? 0) - (a.timeUpdated ?? 0);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
platform: "opencode",
|
|
107
|
+
sessions,
|
|
108
|
+
agents,
|
|
109
|
+
activeCount: agents.filter((a) => a.state === "active").length,
|
|
110
|
+
source: "opencode.db"
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function openReadonlyDb(dbPath, DatabaseSync) {
|
|
115
|
+
return new DatabaseSync(dbPath, { readOnly: true });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Read recent OpenCode sessions (read-only). Inject openDatabase for tests.
|
|
120
|
+
*/
|
|
121
|
+
export async function buildOpenCodeActivity({
|
|
122
|
+
homeDir = resolveHomeDir(),
|
|
123
|
+
limit = DEFAULT_LIMIT,
|
|
124
|
+
activeWindowMs = ACTIVE_WINDOW_MS,
|
|
125
|
+
nowMs = Date.now(),
|
|
126
|
+
exists = pathExists,
|
|
127
|
+
openDatabase = null,
|
|
128
|
+
DatabaseSync = null
|
|
129
|
+
} = {}) {
|
|
130
|
+
const dbPath = resolveOpenCodeDbPath(homeDir);
|
|
131
|
+
if (!(await exists(dbPath))) {
|
|
132
|
+
return {
|
|
133
|
+
ok: true,
|
|
134
|
+
available: false,
|
|
135
|
+
note: "OpenCode session DB not found.",
|
|
136
|
+
dbPath,
|
|
137
|
+
platform: "opencode",
|
|
138
|
+
sessions: [],
|
|
139
|
+
agents: [],
|
|
140
|
+
activeCount: 0,
|
|
141
|
+
generatedAt: new Date(nowMs).toISOString()
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let rows = [];
|
|
146
|
+
try {
|
|
147
|
+
let Database = DatabaseSync;
|
|
148
|
+
if (!Database) {
|
|
149
|
+
const prior = process.emitWarning;
|
|
150
|
+
process.emitWarning = function muted(warning, ...rest) {
|
|
151
|
+
const msg = typeof warning === "string" ? warning : (warning?.message ?? "");
|
|
152
|
+
if (/SQLite is an experimental feature/i.test(String(msg))) return;
|
|
153
|
+
return prior.apply(process, [warning, ...rest]);
|
|
154
|
+
};
|
|
155
|
+
try {
|
|
156
|
+
({ DatabaseSync: Database } = await import("node:sqlite"));
|
|
157
|
+
} finally {
|
|
158
|
+
process.emitWarning = prior;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const open = openDatabase ?? ((path) => openReadonlyDb(path, Database));
|
|
162
|
+
const db = open(dbPath);
|
|
163
|
+
try {
|
|
164
|
+
const stmt = db.prepare(`
|
|
165
|
+
SELECT id, parent_id, title, agent, model, time_created, time_updated, time_archived
|
|
166
|
+
FROM session
|
|
167
|
+
ORDER BY time_updated DESC
|
|
168
|
+
LIMIT ?
|
|
169
|
+
`);
|
|
170
|
+
rows = stmt.all(Math.max(1, Math.min(Number(limit) || DEFAULT_LIMIT, 200)));
|
|
171
|
+
} finally {
|
|
172
|
+
db.close?.();
|
|
173
|
+
}
|
|
174
|
+
} catch (error) {
|
|
175
|
+
return {
|
|
176
|
+
ok: false,
|
|
177
|
+
available: false,
|
|
178
|
+
note: `Could not read OpenCode DB: ${error?.message ?? error}`,
|
|
179
|
+
dbPath,
|
|
180
|
+
platform: "opencode",
|
|
181
|
+
sessions: [],
|
|
182
|
+
agents: [],
|
|
183
|
+
activeCount: 0,
|
|
184
|
+
generatedAt: new Date(nowMs).toISOString()
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const mapped = mapSessionRowsToActivity(rows, { nowMs, activeWindowMs });
|
|
189
|
+
return {
|
|
190
|
+
ok: true,
|
|
191
|
+
available: true,
|
|
192
|
+
note: "Live OpenCode sessions (declared parent→child). Not Cursor/Claude live.",
|
|
193
|
+
dbPath,
|
|
194
|
+
...mapped,
|
|
195
|
+
generatedAt: new Date(nowMs).toISOString()
|
|
196
|
+
};
|
|
197
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declared / known model catalogs per platform (read-only discovery).
|
|
3
|
+
* Not a live marketplace scrape — what is configured or known-safe on disk.
|
|
4
|
+
*/
|
|
5
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { resolveHomeDir } from "../paths.js";
|
|
8
|
+
import { parseCodexDefaultModel, parseFrontmatterModel } from "./fleet-platforms.js";
|
|
9
|
+
|
|
10
|
+
export const CLAUDE_TIERS = Object.freeze(["opus", "sonnet", "haiku"]);
|
|
11
|
+
|
|
12
|
+
export const CURSOR_AGENT_MODELS = Object.freeze(["inherit", "fast"]);
|
|
13
|
+
|
|
14
|
+
async function readJson(path, read) {
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(await read(path, "utf8"));
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function collectOpenCodeModels(homeDir, read) {
|
|
23
|
+
const path = join(homeDir, ".config", "opencode", "opencode.json");
|
|
24
|
+
const config = await readJson(path, read);
|
|
25
|
+
const ids = new Set();
|
|
26
|
+
if (typeof config?.model === "string") ids.add(config.model);
|
|
27
|
+
for (const raw of Object.values(config?.agent ?? config?.agents ?? {})) {
|
|
28
|
+
if (typeof raw?.model === "string") ids.add(raw.model);
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
platform: "opencode",
|
|
32
|
+
kind: "multi",
|
|
33
|
+
path,
|
|
34
|
+
available: [...ids].sort(),
|
|
35
|
+
enabled: typeof config?.model === "string" ? [config.model] : [],
|
|
36
|
+
note: "From opencode.json (declared agents + default)."
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function collectClaudeModels(homeDir, read, list = readdir) {
|
|
41
|
+
const settingsPath = join(homeDir, ".claude", "settings.json");
|
|
42
|
+
const settings = await readJson(settingsPath, read);
|
|
43
|
+
const enabled = [];
|
|
44
|
+
if (typeof settings?.model === "string") enabled.push(settings.model);
|
|
45
|
+
try {
|
|
46
|
+
const dir = join(homeDir, ".claude", "agents");
|
|
47
|
+
for (const name of await list(dir)) {
|
|
48
|
+
if (!name.endsWith(".md")) continue;
|
|
49
|
+
const meta = parseFrontmatterModel(await read(join(dir, name), "utf8"));
|
|
50
|
+
if (meta.model) enabled.push(meta.model);
|
|
51
|
+
}
|
|
52
|
+
} catch { /* missing */ }
|
|
53
|
+
return {
|
|
54
|
+
platform: "claude",
|
|
55
|
+
kind: "multi",
|
|
56
|
+
path: settingsPath,
|
|
57
|
+
available: [...CLAUDE_TIERS],
|
|
58
|
+
enabled: [...new Set(enabled)],
|
|
59
|
+
note: "Claude Code tiers (opus/sonnet/haiku) + agents frontmatter."
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function collectCodexModels(homeDir, read) {
|
|
64
|
+
const path = join(homeDir, ".codex", "config.toml");
|
|
65
|
+
let current = null;
|
|
66
|
+
try {
|
|
67
|
+
current = parseCodexDefaultModel(await read(path, "utf8"));
|
|
68
|
+
} catch { /* missing */ }
|
|
69
|
+
return {
|
|
70
|
+
platform: "codex",
|
|
71
|
+
kind: "single",
|
|
72
|
+
path,
|
|
73
|
+
available: current ? [current] : [],
|
|
74
|
+
enabled: current ? [current] : [],
|
|
75
|
+
note: "Single default model in config.toml (no per-phase minions)."
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function collectCursorModels(homeDir, read, list = readdir) {
|
|
80
|
+
const cliPath = join(homeDir, ".cursor", "cli-config.json");
|
|
81
|
+
const cli = await readJson(cliPath, read);
|
|
82
|
+
const cliModel = cli?.model?.modelId ?? cli?.model?.displayModelId ?? null;
|
|
83
|
+
const agentModels = new Set();
|
|
84
|
+
try {
|
|
85
|
+
const dir = join(homeDir, ".cursor", "agents");
|
|
86
|
+
for (const name of await list(dir)) {
|
|
87
|
+
if (!name.endsWith(".md")) continue;
|
|
88
|
+
const meta = parseFrontmatterModel(await read(join(dir, name), "utf8"));
|
|
89
|
+
if (meta.model) agentModels.add(meta.model);
|
|
90
|
+
}
|
|
91
|
+
} catch { /* missing */ }
|
|
92
|
+
return {
|
|
93
|
+
platform: "cursor",
|
|
94
|
+
kind: "multi",
|
|
95
|
+
path: join(homeDir, ".cursor", "agents"),
|
|
96
|
+
available: [...CURSOR_AGENT_MODELS, ...(cliModel ? [cliModel] : [])],
|
|
97
|
+
enabled: [...agentModels],
|
|
98
|
+
cliDefault: cliModel,
|
|
99
|
+
note: "Agent frontmatter usually inherit/fast. Auto chat model is IDE-managed (see cli-config)."
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Catalog of available + enabled models for each tooling surface.
|
|
105
|
+
*/
|
|
106
|
+
export async function buildFleetModelsCatalog({
|
|
107
|
+
homeDir = resolveHomeDir(),
|
|
108
|
+
read = readFile,
|
|
109
|
+
list = readdir
|
|
110
|
+
} = {}) {
|
|
111
|
+
const platforms = await Promise.all([
|
|
112
|
+
collectOpenCodeModels(homeDir, read),
|
|
113
|
+
collectClaudeModels(homeDir, read, list),
|
|
114
|
+
collectCodexModels(homeDir, read),
|
|
115
|
+
collectCursorModels(homeDir, read, list)
|
|
116
|
+
]);
|
|
117
|
+
return {
|
|
118
|
+
ok: true,
|
|
119
|
+
kind: "catalog",
|
|
120
|
+
note: "Available = known/declared for that tool. Enabled = currently referenced on disk.",
|
|
121
|
+
platforms,
|
|
122
|
+
generatedAt: new Date().toISOString()
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function formatFleetModelsText(catalog) {
|
|
127
|
+
const lines = ["Fleet models (available · enabled)", ""];
|
|
128
|
+
for (const p of catalog.platforms ?? []) {
|
|
129
|
+
lines.push(`${p.platform} · ${p.kind}${p.cliDefault ? ` · cli ${p.cliDefault}` : ""}`);
|
|
130
|
+
lines.push(` available · ${(p.available ?? []).join(", ") || "—"}`);
|
|
131
|
+
lines.push(` enabled · ${(p.enabled ?? []).join(", ") || "—"}`);
|
|
132
|
+
if (p.note) lines.push(` ${p.note}`);
|
|
133
|
+
lines.push("");
|
|
134
|
+
}
|
|
135
|
+
lines.push(catalog.note ?? "");
|
|
136
|
+
return lines.join("\n").trimEnd();
|
|
137
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declared fleets for Claude / Codex / Cursor agents (public configs only).
|
|
3
|
+
*/
|
|
4
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
|
|
7
|
+
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---/;
|
|
8
|
+
|
|
9
|
+
export function parseFrontmatterModel(raw) {
|
|
10
|
+
const text = String(raw ?? "");
|
|
11
|
+
const match = text.match(FRONTMATTER_RE);
|
|
12
|
+
if (!match) return { model: null, name: null };
|
|
13
|
+
const block = match[1];
|
|
14
|
+
const modelLine = block.match(/^model:\s*(.+)$/m);
|
|
15
|
+
const nameLine = block.match(/^name:\s*(.+)$/m);
|
|
16
|
+
const model = modelLine ? modelLine[1].trim().replace(/^["']|["']$/g, "") : null;
|
|
17
|
+
const name = nameLine ? nameLine[1].trim().replace(/^["']|["']$/g, "") : null;
|
|
18
|
+
return { model, name };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function replaceFrontmatterModel(raw, nextModel) {
|
|
22
|
+
const text = String(raw ?? "");
|
|
23
|
+
const match = text.match(FRONTMATTER_RE);
|
|
24
|
+
if (!match) {
|
|
25
|
+
throw new Error("Agent file has no YAML frontmatter to update.");
|
|
26
|
+
}
|
|
27
|
+
const block = match[1];
|
|
28
|
+
let nextBlock;
|
|
29
|
+
if (/^model:\s*.+$/m.test(block)) {
|
|
30
|
+
nextBlock = block.replace(/^model:\s*.+$/m, `model: ${nextModel}`);
|
|
31
|
+
} else {
|
|
32
|
+
nextBlock = `${block.trimEnd()}\nmodel: ${nextModel}`;
|
|
33
|
+
}
|
|
34
|
+
return text.replace(FRONTMATTER_RE, `---\n${nextBlock}\n---`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function parseCodexDefaultModel(tomlText) {
|
|
38
|
+
const match = String(tomlText ?? "").match(/^\s*model\s*=\s*"([^"]+)"/m);
|
|
39
|
+
return match ? match[1] : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function replaceCodexDefaultModel(tomlText, nextModel) {
|
|
43
|
+
const text = String(tomlText ?? "");
|
|
44
|
+
if (!/^\s*model\s*=\s*"[^"]*"/m.test(text)) {
|
|
45
|
+
throw new Error("Codex config.toml has no top-level model = \"...\" line.");
|
|
46
|
+
}
|
|
47
|
+
return text.replace(/^\s*model\s*=\s*"[^"]*"/m, `model = "${nextModel}"`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function minionRole(id) {
|
|
51
|
+
if (id === "sdd-apply") return "executor";
|
|
52
|
+
if (id === "sdd-explore") return "explorer";
|
|
53
|
+
if (id === "sdd-verify") return "verifier";
|
|
54
|
+
return id.startsWith("sdd-") ? "specialist" : "minion";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function listAgentFiles(dir, read = readFile, list = readdir) {
|
|
58
|
+
try {
|
|
59
|
+
const names = await list(dir);
|
|
60
|
+
const out = [];
|
|
61
|
+
for (const name of names) {
|
|
62
|
+
if (!name.endsWith(".md")) continue;
|
|
63
|
+
const path = join(dir, name);
|
|
64
|
+
const raw = await read(path, "utf8");
|
|
65
|
+
const meta = parseFrontmatterModel(raw);
|
|
66
|
+
const id = meta.name || name.replace(/\.md$/, "");
|
|
67
|
+
if (!id.startsWith("sdd-")) continue;
|
|
68
|
+
out.push({
|
|
69
|
+
id,
|
|
70
|
+
model: meta.model,
|
|
71
|
+
modelShort: meta.model,
|
|
72
|
+
role: minionRole(id),
|
|
73
|
+
mode: "subagent",
|
|
74
|
+
path,
|
|
75
|
+
opaque: meta.model === "inherit" || meta.model == null
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return out.sort((a, b) => a.id.localeCompare(b.id));
|
|
79
|
+
} catch {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function buildClaudeFleet({ homeDir, read = readFile, list = readdir } = {}) {
|
|
85
|
+
const settingsPath = join(homeDir, ".claude", "settings.json");
|
|
86
|
+
let defaultModel = null;
|
|
87
|
+
try {
|
|
88
|
+
const settings = JSON.parse(await read(settingsPath, "utf8"));
|
|
89
|
+
defaultModel = typeof settings?.model === "string" ? settings.model : null;
|
|
90
|
+
} catch {
|
|
91
|
+
defaultModel = null;
|
|
92
|
+
}
|
|
93
|
+
const minions = await listAgentFiles(join(homeDir, ".claude", "agents"), read, list);
|
|
94
|
+
return {
|
|
95
|
+
platform: "claude",
|
|
96
|
+
orchestrator: {
|
|
97
|
+
id: "default",
|
|
98
|
+
model: defaultModel,
|
|
99
|
+
modelShort: defaultModel,
|
|
100
|
+
mode: "primary",
|
|
101
|
+
opaque: false
|
|
102
|
+
},
|
|
103
|
+
minions: minions.map((m) => ({
|
|
104
|
+
...m,
|
|
105
|
+
model: m.model ?? defaultModel,
|
|
106
|
+
modelShort: m.modelShort ?? defaultModel,
|
|
107
|
+
opaque: false
|
|
108
|
+
})),
|
|
109
|
+
opaque: false,
|
|
110
|
+
writable: true,
|
|
111
|
+
note: "Declared Claude settings + agent frontmatter models.",
|
|
112
|
+
source: "claude"
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function buildCodexFleet({ homeDir, read = readFile } = {}) {
|
|
117
|
+
const configPath = join(homeDir, ".codex", "config.toml");
|
|
118
|
+
let model = null;
|
|
119
|
+
try {
|
|
120
|
+
model = parseCodexDefaultModel(await read(configPath, "utf8"));
|
|
121
|
+
} catch {
|
|
122
|
+
model = null;
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
platform: "codex",
|
|
126
|
+
orchestrator: {
|
|
127
|
+
id: "default",
|
|
128
|
+
model,
|
|
129
|
+
modelShort: model,
|
|
130
|
+
mode: "primary",
|
|
131
|
+
opaque: model == null
|
|
132
|
+
},
|
|
133
|
+
minions: [],
|
|
134
|
+
opaque: model == null,
|
|
135
|
+
writable: true,
|
|
136
|
+
configPath,
|
|
137
|
+
note: "Codex default model from ~/.codex/config.toml (no parent→child live topology).",
|
|
138
|
+
source: "codex"
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function buildCursorAgentsFleet({ homeDir, read = readFile, list = readdir } = {}) {
|
|
143
|
+
const minions = await listAgentFiles(join(homeDir, ".cursor", "agents"), read, list);
|
|
144
|
+
return {
|
|
145
|
+
platform: "cursor",
|
|
146
|
+
orchestrator: {
|
|
147
|
+
id: "auto",
|
|
148
|
+
model: null,
|
|
149
|
+
modelShort: null,
|
|
150
|
+
mode: "primary",
|
|
151
|
+
opaque: true
|
|
152
|
+
},
|
|
153
|
+
minions: minions.map((m) => ({
|
|
154
|
+
id: m.id,
|
|
155
|
+
model: m.model,
|
|
156
|
+
modelShort: m.modelShort,
|
|
157
|
+
role: m.role,
|
|
158
|
+
mode: "subagent",
|
|
159
|
+
opaque: true
|
|
160
|
+
})),
|
|
161
|
+
opaque: true,
|
|
162
|
+
writable: false,
|
|
163
|
+
note: "Cursor Auto is IDE-managed. Subagents typically use model: inherit — change models in Cursor UI.",
|
|
164
|
+
source: "cursor-agents"
|
|
165
|
+
};
|
|
166
|
+
}
|