@sayknow-cli/coding-agent 0.2.4 → 0.2.5
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/dist/types/cli/migrate-cli.d.ts +20 -0
- package/dist/types/cli/update-cli.d.ts +3 -0
- package/dist/types/commands/migrate.d.ts +33 -0
- package/dist/types/config/keybindings.d.ts +4 -0
- package/dist/types/config/model-registry.d.ts +3 -0
- package/dist/types/config/models-config-schema.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +27 -0
- package/dist/types/harness-control-plane/storage.d.ts +2 -1
- package/dist/types/hooks/skill-state.d.ts +12 -4
- package/dist/types/lsp/startup-events.d.ts +1 -0
- package/dist/types/migrate/action-planner.d.ts +11 -0
- package/dist/types/migrate/adapters/claude-code.d.ts +2 -0
- package/dist/types/migrate/adapters/codex.d.ts +5 -0
- package/dist/types/migrate/adapters/index.d.ts +45 -0
- package/dist/types/migrate/adapters/opencode.d.ts +2 -0
- package/dist/types/migrate/executor.d.ts +2 -0
- package/dist/types/migrate/mcp-mapper.d.ts +20 -0
- package/dist/types/migrate/report.d.ts +18 -0
- package/dist/types/migrate/skill-normalizer.d.ts +27 -0
- package/dist/types/migrate/types.d.ts +126 -0
- package/dist/types/modes/components/custom-editor.d.ts +1 -1
- package/dist/types/modes/components/welcome.d.ts +3 -1
- package/dist/types/modes/interactive-mode.d.ts +3 -0
- package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -0
- package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +1 -1
- package/dist/types/research-plan/index.d.ts +1 -0
- package/dist/types/research-plan/ledger.d.ts +33 -0
- package/dist/types/rlm/artifacts.d.ts +1 -1
- package/dist/types/runtime-mcp/config-writer.d.ts +26 -0
- package/dist/types/skc-runtime/deep-interview-recorder.d.ts +2 -0
- package/dist/types/skc-runtime/deep-interview-runtime.d.ts +2 -2
- package/dist/types/skc-runtime/goal-mode-request.d.ts +1 -1
- package/dist/types/skc-runtime/session-layout.d.ts +59 -0
- package/dist/types/skc-runtime/session-resolution.d.ts +47 -0
- package/dist/types/skc-runtime/state-graph.d.ts +1 -1
- package/dist/types/skc-runtime/state-runtime.d.ts +5 -4
- package/dist/types/skc-runtime/state-schema.d.ts +2 -0
- package/dist/types/skc-runtime/state-writer.d.ts +36 -7
- package/dist/types/skc-runtime/tmux-sessions.d.ts +2 -0
- package/dist/types/skc-runtime/ultragoal-runtime.d.ts +7 -4
- package/dist/types/skc-runtime/workflow-command-ref.d.ts +1 -1
- package/dist/types/skc-runtime/workflow-manifest.d.ts +1 -1
- package/dist/types/skill-state/active-state.d.ts +6 -11
- package/dist/types/skill-state/canonical-skills.d.ts +3 -0
- package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +5 -0
- package/dist/types/skill-state/workflow-hud.d.ts +2 -0
- package/dist/types/task/spawn-gate.d.ts +1 -10
- package/package.json +7 -7
- package/scripts/build-binary.ts +0 -7
- package/src/cli/migrate-cli.ts +106 -0
- package/src/cli/setup-cli.ts +14 -1
- package/src/cli/update-cli.ts +53 -3
- package/src/cli.ts +1 -0
- package/src/commands/deep-interview.ts +2 -2
- package/src/commands/launch.ts +1 -1
- package/src/commands/migrate.ts +46 -0
- package/src/commands/state.ts +2 -1
- package/src/commands/team.ts +7 -3
- package/src/config/model-registry.ts +9 -2
- package/src/config/model-resolver.ts +13 -2
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +17 -0
- package/src/coordinator-mcp/policy.ts +10 -2
- package/src/defaults/skc/extensions/grok-cli-vendor/biome.json +0 -1
- package/src/defaults/skc/skills/deep-interview/SKILL.md +30 -24
- package/src/defaults/skc/skills/ralplan/SKILL.md +10 -4
- package/src/defaults/skc/skills/team/SKILL.md +51 -47
- package/src/defaults/skc/skills/ultragoal/SKILL.md +17 -13
- package/src/exec/bash-executor.ts +3 -1
- package/src/extensibility/custom-commands/loader.ts +0 -7
- package/src/extensibility/skc-plugins/injection.ts +23 -4
- package/src/extensibility/skc-plugins/state.ts +16 -1
- package/src/harness-control-plane/storage.ts +14 -4
- package/src/hooks/native-skill-hook.ts +38 -12
- package/src/hooks/skill-state.ts +178 -83
- package/src/internal-urls/docs-index.generated.ts +11 -9
- package/src/lsp/startup-events.ts +24 -0
- package/src/migrate/action-planner.ts +318 -0
- package/src/migrate/adapters/claude-code.ts +39 -0
- package/src/migrate/adapters/codex.ts +70 -0
- package/src/migrate/adapters/index.ts +277 -0
- package/src/migrate/adapters/opencode.ts +52 -0
- package/src/migrate/executor.ts +81 -0
- package/src/migrate/mcp-mapper.ts +152 -0
- package/src/migrate/report.ts +104 -0
- package/src/migrate/skill-normalizer.ts +80 -0
- package/src/migrate/types.ts +163 -0
- package/src/modes/bridge/bridge-mode.ts +2 -2
- package/src/modes/components/custom-editor.ts +30 -20
- package/src/modes/components/welcome.ts +20 -9
- package/src/modes/controllers/input-controller.ts +21 -3
- package/src/modes/interactive-mode.ts +27 -19
- package/src/modes/prompt-action-autocomplete.ts +11 -1
- package/src/modes/rpc/rpc-mode.ts +2 -2
- package/src/modes/shared/agent-wire/unattended-audit.ts +3 -2
- package/src/prompts/agents/init.md +1 -1
- package/src/prompts/system/plan-mode-active.md +1 -1
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/search.md +1 -1
- package/src/prompts/tools/task.md +1 -2
- package/src/research-plan/index.ts +1 -0
- package/src/research-plan/ledger.ts +177 -0
- package/src/rlm/artifacts.ts +12 -3
- package/src/rlm/index.ts +7 -0
- package/src/runtime-mcp/config-writer.ts +46 -0
- package/src/session/agent-session.ts +43 -41
- package/src/session/session-manager.ts +19 -2
- package/src/setup/hermes/templates/operator-instructions.v1.md +8 -0
- package/src/setup/hermes-setup.ts +1 -1
- package/src/skc-runtime/deep-interview-recorder.ts +51 -18
- package/src/skc-runtime/deep-interview-runtime.ts +49 -23
- package/src/skc-runtime/goal-mode-request.ts +26 -11
- package/src/skc-runtime/launch-tmux.ts +68 -15
- package/src/skc-runtime/ralplan-runtime.ts +79 -50
- package/src/skc-runtime/session-layout.ts +180 -0
- package/src/skc-runtime/session-resolution.ts +217 -0
- package/src/skc-runtime/state-graph.ts +1 -2
- package/src/skc-runtime/state-migrations.ts +1 -0
- package/src/skc-runtime/state-runtime.ts +237 -114
- package/src/skc-runtime/state-schema.ts +2 -0
- package/src/skc-runtime/state-writer.ts +310 -42
- package/src/skc-runtime/team-runtime.ts +43 -19
- package/src/skc-runtime/tmux-sessions.ts +43 -2
- package/src/skc-runtime/ultragoal-guard.ts +45 -2
- package/src/skc-runtime/ultragoal-runtime.ts +121 -41
- package/src/skc-runtime/workflow-command-ref.ts +1 -2
- package/src/skc-runtime/workflow-manifest.ts +1 -2
- package/src/skill-state/active-state.ts +116 -129
- package/src/skill-state/canonical-skills.ts +4 -0
- package/src/skill-state/deep-interview-mutation-guard.ts +238 -111
- package/src/skill-state/workflow-hud.ts +4 -2
- package/src/skill-state/workflow-state-contract.ts +3 -3
- package/src/slash-commands/builtin-registry.ts +8 -4
- package/src/system-prompt.ts +11 -9
- package/src/task/agents.ts +1 -22
- package/src/task/index.ts +1 -41
- package/src/task/spawn-gate.ts +1 -38
- package/src/task/types.ts +1 -1
- package/src/tools/ask.ts +34 -12
- package/src/tools/ast-edit.ts +2 -2
- package/src/tools/computer.ts +58 -4
- package/src/utils/edit-mode.ts +1 -1
- package/dist/types/extensibility/custom-commands/bundled/review/index.d.ts +0 -10
- package/src/extensibility/custom-commands/bundled/review/index.ts +0 -456
- package/src/prompts/agents/explore.md +0 -58
- package/src/prompts/agents/plan.md +0 -49
- package/src/prompts/agents/reviewer.md +0 -141
- package/src/prompts/agents/task.md +0 -16
- package/src/prompts/review-request.md +0 -70
|
@@ -11,3 +11,27 @@ export type LspStartupEvent =
|
|
|
11
11
|
type: "failed";
|
|
12
12
|
error: string;
|
|
13
13
|
};
|
|
14
|
+
const OPTIONAL_STARTUP_FAILURE_SERVERS = new Set(["rust-analyzer"]);
|
|
15
|
+
|
|
16
|
+
function isOptionalStartupFailure(server: LspStartupServerInfo): boolean {
|
|
17
|
+
return server.status === "error" && OPTIONAL_STARTUP_FAILURE_SERVERS.has(server.name);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getLspStartupWarningMessage(event: LspStartupEvent): string | null {
|
|
21
|
+
if (event.type === "failed") {
|
|
22
|
+
return "LSP startup failed. It will retry lazily on write.";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const failedServers = event.servers.filter(server => server.status === "error" && !isOptionalStartupFailure(server));
|
|
26
|
+
|
|
27
|
+
if (failedServers.length === 1) {
|
|
28
|
+
return `LSP startup failed for ${failedServers[0].name}. It will retry lazily on write.`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (failedServers.length > 1) {
|
|
32
|
+
const failedNames = failedServers.map(server => server.name).join(", ");
|
|
33
|
+
return `LSP startup failed for ${failedNames}. It will retry lazily on write.`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Central migration action planner.
|
|
3
|
+
*
|
|
4
|
+
* Reads destination state ONCE and produces an immutable list of actions that
|
|
5
|
+
* both dry-run and live execution consume unchanged. This is the single place
|
|
6
|
+
* that decides add/update/skip/fail, destinations, and warnings, guaranteeing
|
|
7
|
+
* dry-run/live parity.
|
|
8
|
+
*/
|
|
9
|
+
import * as fs from "node:fs/promises";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import { isEnoent, parseFrontmatter } from "@sayknow-cli/utils";
|
|
12
|
+
import { readMCPConfigFile, validateServerName } from "../runtime-mcp/config-writer";
|
|
13
|
+
import { mapMcpEntry } from "./mcp-mapper";
|
|
14
|
+
import { slugify } from "./skill-normalizer";
|
|
15
|
+
import type { AdapterResult, MigrateAction, MigrateDestinations, MigrateWarning } from "./types";
|
|
16
|
+
|
|
17
|
+
export interface PlanInput {
|
|
18
|
+
results: AdapterResult[];
|
|
19
|
+
destinations: MigrateDestinations;
|
|
20
|
+
force: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface PlanOutput {
|
|
24
|
+
actions: MigrateAction[];
|
|
25
|
+
warnings: MigrateWarning[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface DestSkillIndex {
|
|
29
|
+
/** slug -> kind of existing destination entry. */
|
|
30
|
+
slugs: Map<string, "dir-with-skill" | "stale-dir" | "occupied">;
|
|
31
|
+
/** effective loaded name -> owning slug. */
|
|
32
|
+
effectiveNames: Map<string, string>;
|
|
33
|
+
/** The skills root exists but is unsafe to write through. */
|
|
34
|
+
rootUnsafe?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function indexDestinationSkills(skillsDir: string): Promise<DestSkillIndex> {
|
|
38
|
+
const index: DestSkillIndex = { slugs: new Map(), effectiveNames: new Map() };
|
|
39
|
+
let rootStat: Awaited<ReturnType<typeof fs.lstat>>;
|
|
40
|
+
try {
|
|
41
|
+
rootStat = await fs.lstat(skillsDir);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (isEnoent(error)) return index;
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) return { ...index, rootUnsafe: true };
|
|
47
|
+
const entries = await fs.readdir(skillsDir, { withFileTypes: true });
|
|
48
|
+
for (const entry of entries) {
|
|
49
|
+
const name = String(entry.name);
|
|
50
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) {
|
|
51
|
+
index.slugs.set(name, "occupied");
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const slug = name;
|
|
55
|
+
const skillFile = path.join(skillsDir, slug, "SKILL.md");
|
|
56
|
+
let content: string | undefined;
|
|
57
|
+
try {
|
|
58
|
+
const skillFileStat = await fs.lstat(skillFile);
|
|
59
|
+
if (!skillFileStat.isFile() || skillFileStat.isSymbolicLink()) {
|
|
60
|
+
index.slugs.set(slug, "occupied");
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
content = await fs.readFile(skillFile, "utf-8");
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (isEnoent(error)) {
|
|
66
|
+
index.slugs.set(slug, "stale-dir");
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
index.slugs.set(slug, "dir-with-skill");
|
|
72
|
+
const { frontmatter } = parseFrontmatter(content, { level: "off" });
|
|
73
|
+
const effective =
|
|
74
|
+
typeof frontmatter.name === "string" && frontmatter.name.trim() ? slugify(frontmatter.name) : slug;
|
|
75
|
+
index.effectiveNames.set(effective, slug);
|
|
76
|
+
}
|
|
77
|
+
return index;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function planMigration(input: PlanInput): Promise<PlanOutput> {
|
|
81
|
+
const actions: MigrateAction[] = [];
|
|
82
|
+
const warnings: MigrateWarning[] = [];
|
|
83
|
+
|
|
84
|
+
// 1. Source-level diagnostics become `source`-typed actions.
|
|
85
|
+
for (const result of input.results) {
|
|
86
|
+
for (const diag of result.diagnostics) {
|
|
87
|
+
actions.push({
|
|
88
|
+
source: diag.source,
|
|
89
|
+
type: "source",
|
|
90
|
+
operation: diag.status.startsWith("failed") ? "fail" : "skip",
|
|
91
|
+
status: diag.status,
|
|
92
|
+
reason: diag.message,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 2. MCP actions — read the destination config once.
|
|
98
|
+
const existingConfig = await readMCPConfigFile(input.destinations.mcpConfigPath).catch(() => null);
|
|
99
|
+
const mcpInvalid = existingConfig === null;
|
|
100
|
+
const existingServers = new Set(Object.keys(existingConfig?.mcpServers ?? {}));
|
|
101
|
+
const disabledServers = new Set(existingConfig?.disabledServers ?? []);
|
|
102
|
+
|
|
103
|
+
for (const result of input.results) {
|
|
104
|
+
for (const candidate of result.mcpCandidates) {
|
|
105
|
+
const nameError = validateServerName(candidate.name);
|
|
106
|
+
if (nameError) {
|
|
107
|
+
actions.push({
|
|
108
|
+
source: candidate.source,
|
|
109
|
+
type: "mcp",
|
|
110
|
+
name: candidate.name,
|
|
111
|
+
operation: "fail",
|
|
112
|
+
status: "failed_invalid_source",
|
|
113
|
+
reason: nameError,
|
|
114
|
+
});
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const mapped = mapMcpEntry(candidate.source, candidate.name, candidate.raw);
|
|
118
|
+
if (!mapped.ok) {
|
|
119
|
+
actions.push({
|
|
120
|
+
source: candidate.source,
|
|
121
|
+
type: "mcp",
|
|
122
|
+
name: candidate.name,
|
|
123
|
+
operation: mapped.status.startsWith("failed") ? "fail" : "skip",
|
|
124
|
+
status: mapped.status,
|
|
125
|
+
reason: mapped.reason,
|
|
126
|
+
});
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
for (const w of mapped.warnings)
|
|
130
|
+
warnings.push({ source: candidate.source, type: "mcp", name: candidate.name, message: w });
|
|
131
|
+
|
|
132
|
+
if (mcpInvalid) {
|
|
133
|
+
actions.push({
|
|
134
|
+
source: candidate.source,
|
|
135
|
+
type: "mcp",
|
|
136
|
+
name: candidate.name,
|
|
137
|
+
destination: input.destinations.mcpConfigPath,
|
|
138
|
+
operation: "fail",
|
|
139
|
+
status: "failed_invalid_destination",
|
|
140
|
+
reason: "destination mcp.json is malformed",
|
|
141
|
+
});
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const exists = existingServers.has(candidate.name);
|
|
146
|
+
if (exists && !input.force) {
|
|
147
|
+
actions.push({
|
|
148
|
+
source: candidate.source,
|
|
149
|
+
type: "mcp",
|
|
150
|
+
name: candidate.name,
|
|
151
|
+
destination: input.destinations.mcpConfigPath,
|
|
152
|
+
operation: "skip",
|
|
153
|
+
status: "skipped_exists",
|
|
154
|
+
});
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const actionWarnings = [...mapped.warnings];
|
|
158
|
+
if (exists && disabledServers.has(candidate.name)) {
|
|
159
|
+
const msg = `disabled MCP state preserved for "${candidate.name}"`;
|
|
160
|
+
actionWarnings.push(msg);
|
|
161
|
+
warnings.push({ source: candidate.source, type: "mcp", name: candidate.name, message: msg });
|
|
162
|
+
}
|
|
163
|
+
actions.push({
|
|
164
|
+
source: candidate.source,
|
|
165
|
+
type: "mcp",
|
|
166
|
+
name: candidate.name,
|
|
167
|
+
destination: input.destinations.mcpConfigPath,
|
|
168
|
+
operation: exists ? "update" : "create",
|
|
169
|
+
status: exists ? "updated" : "imported",
|
|
170
|
+
warnings: actionWarnings.length > 0 ? actionWarnings : undefined,
|
|
171
|
+
mcp: { config: mapped.config, force: input.force },
|
|
172
|
+
});
|
|
173
|
+
// Track so an intra-run duplicate of the same name doesn't double-write.
|
|
174
|
+
existingServers.add(candidate.name);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 3. Skill actions — index the destination skills tree once.
|
|
179
|
+
const destIndex = await indexDestinationSkills(input.destinations.skillsDir);
|
|
180
|
+
const plannedSlugs = new Map<string, string>(); // slug -> source (intra-run)
|
|
181
|
+
const plannedEffective = new Map<string, string>(); // effective -> slug (intra-run)
|
|
182
|
+
|
|
183
|
+
for (const result of input.results) {
|
|
184
|
+
for (const candidate of result.skillCandidates) {
|
|
185
|
+
for (const w of candidate.warnings) {
|
|
186
|
+
warnings.push({ source: candidate.source, type: "skill", name: candidate.slug, message: w });
|
|
187
|
+
}
|
|
188
|
+
const slug = candidate.slug;
|
|
189
|
+
const destination = path.join(input.destinations.skillsDir, slug, "SKILL.md");
|
|
190
|
+
const base = {
|
|
191
|
+
source: candidate.source,
|
|
192
|
+
type: "skill" as const,
|
|
193
|
+
name: slug,
|
|
194
|
+
effectiveName: slug,
|
|
195
|
+
destination,
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// Intra-run duplicate slug / effective name. Since effective name == slug, a
|
|
199
|
+
// duplicate always targets the same destination: skip by default; with --force
|
|
200
|
+
// the later (canonical-order) source overwrites the same destination.
|
|
201
|
+
if (plannedSlugs.has(slug) || plannedEffective.has(slug)) {
|
|
202
|
+
if (input.force) {
|
|
203
|
+
actions.push({
|
|
204
|
+
...base,
|
|
205
|
+
operation: "update",
|
|
206
|
+
status: "updated",
|
|
207
|
+
reason: "duplicate within this run (overwritten under --force)",
|
|
208
|
+
skill: { content: candidate.content },
|
|
209
|
+
});
|
|
210
|
+
} else {
|
|
211
|
+
actions.push({
|
|
212
|
+
...base,
|
|
213
|
+
operation: "skip",
|
|
214
|
+
status: "skipped_exists",
|
|
215
|
+
reason: "duplicate within this run",
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const existingKind = destIndex.slugs.get(slug);
|
|
222
|
+
const effectiveOwner = destIndex.effectiveNames.get(slug);
|
|
223
|
+
|
|
224
|
+
if (destIndex.rootUnsafe) {
|
|
225
|
+
if (!input.force) {
|
|
226
|
+
actions.push({
|
|
227
|
+
...base,
|
|
228
|
+
operation: "skip",
|
|
229
|
+
status: "skipped_exists",
|
|
230
|
+
reason: "skills destination root is not a real directory",
|
|
231
|
+
});
|
|
232
|
+
} else {
|
|
233
|
+
actions.push({
|
|
234
|
+
...base,
|
|
235
|
+
operation: "fail",
|
|
236
|
+
status: "failed_invalid_destination",
|
|
237
|
+
reason: "skills destination root is not a real directory; refusing to write",
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Effective-name collision with a different existing destination skill.
|
|
244
|
+
if (effectiveOwner && effectiveOwner !== slug) {
|
|
245
|
+
if (!input.force) {
|
|
246
|
+
actions.push({
|
|
247
|
+
...base,
|
|
248
|
+
operation: "skip",
|
|
249
|
+
status: "skipped_exists",
|
|
250
|
+
reason: `effective name collides with "${effectiveOwner}"`,
|
|
251
|
+
});
|
|
252
|
+
} else {
|
|
253
|
+
actions.push({
|
|
254
|
+
...base,
|
|
255
|
+
operation: "fail",
|
|
256
|
+
status: "failed_invalid_destination",
|
|
257
|
+
reason: `effective name collides with "${effectiveOwner}"`,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (existingKind === "occupied") {
|
|
264
|
+
if (!input.force) {
|
|
265
|
+
actions.push({
|
|
266
|
+
...base,
|
|
267
|
+
operation: "skip",
|
|
268
|
+
status: "skipped_exists",
|
|
269
|
+
reason: "a non-directory or unsafe file occupies the destination path",
|
|
270
|
+
});
|
|
271
|
+
} else {
|
|
272
|
+
actions.push({
|
|
273
|
+
...base,
|
|
274
|
+
operation: "fail",
|
|
275
|
+
status: "failed_invalid_destination",
|
|
276
|
+
reason: "a non-directory or unsafe file occupies the destination path; refusing to delete",
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (existingKind === "dir-with-skill") {
|
|
283
|
+
if (!input.force) {
|
|
284
|
+
actions.push({ ...base, operation: "skip", status: "skipped_exists" });
|
|
285
|
+
} else {
|
|
286
|
+
actions.push({ ...base, operation: "update", status: "updated", skill: { content: candidate.content } });
|
|
287
|
+
}
|
|
288
|
+
plannedSlugs.set(slug, candidate.source);
|
|
289
|
+
plannedEffective.set(slug, slug);
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (existingKind === "stale-dir") {
|
|
294
|
+
if (!input.force) {
|
|
295
|
+
actions.push({ ...base, operation: "skip", status: "skipped_exists", reason: "stale skill directory" });
|
|
296
|
+
} else {
|
|
297
|
+
actions.push({
|
|
298
|
+
...base,
|
|
299
|
+
operation: "update",
|
|
300
|
+
status: "updated",
|
|
301
|
+
reason: "stale skill directory reused",
|
|
302
|
+
skill: { content: candidate.content },
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
plannedSlugs.set(slug, candidate.source);
|
|
306
|
+
plannedEffective.set(slug, slug);
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Fresh import.
|
|
311
|
+
actions.push({ ...base, operation: "create", status: "imported", skill: { content: candidate.content } });
|
|
312
|
+
plannedSlugs.set(slug, candidate.source);
|
|
313
|
+
plannedEffective.set(slug, slug);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return { actions, warnings };
|
|
318
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code adapter: reads `~/.claude.json` (mcpServers) and `~/.claude/skills`.
|
|
3
|
+
*/
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import type { AdapterResult, McpCandidate } from "../types";
|
|
6
|
+
import { type Adapter, type AdapterOptions, collectSkillDir, parseSourceJson, readSourceText } from "./index";
|
|
7
|
+
|
|
8
|
+
const SOURCE = "claude-code" as const;
|
|
9
|
+
|
|
10
|
+
export const claudeCodeAdapter: Adapter = {
|
|
11
|
+
source: SOURCE,
|
|
12
|
+
async collect({ homeDir }: AdapterOptions): Promise<AdapterResult> {
|
|
13
|
+
const result: AdapterResult = { mcpCandidates: [], skillCandidates: [], diagnostics: [] };
|
|
14
|
+
|
|
15
|
+
const configPath = path.join(homeDir, ".claude.json");
|
|
16
|
+
const read = await readSourceText(configPath, SOURCE, "mcp");
|
|
17
|
+
if ("diagnostic" in read) {
|
|
18
|
+
result.diagnostics.push(read.diagnostic);
|
|
19
|
+
} else {
|
|
20
|
+
const parsed = parseSourceJson(read.text, configPath, SOURCE, "mcp");
|
|
21
|
+
if ("diagnostic" in parsed) {
|
|
22
|
+
result.diagnostics.push(parsed.diagnostic);
|
|
23
|
+
} else {
|
|
24
|
+
const servers = parsed.data.mcpServers;
|
|
25
|
+
if (servers && typeof servers === "object" && !Array.isArray(servers)) {
|
|
26
|
+
for (const [name, raw] of Object.entries(servers as Record<string, unknown>)) {
|
|
27
|
+
result.mcpCandidates.push({ source: SOURCE, name, raw } satisfies McpCandidate);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const skills = await collectSkillDir(path.join(homeDir, ".claude", "skills"), SOURCE);
|
|
34
|
+
result.skillCandidates.push(...skills.candidates);
|
|
35
|
+
result.diagnostics.push(...skills.diagnostics);
|
|
36
|
+
|
|
37
|
+
return result;
|
|
38
|
+
},
|
|
39
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex adapter: reads `~/.codex/config.toml` ([mcp_servers]) and `~/.codex/prompts`.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { TOML } from "bun";
|
|
7
|
+
import type { AdapterResult, McpCandidate, SourceDiagnostic } from "../types";
|
|
8
|
+
import { type Adapter, type AdapterOptions, collectMarkdownPrompts, readSourceText } from "./index";
|
|
9
|
+
|
|
10
|
+
const SOURCE = "codex" as const;
|
|
11
|
+
|
|
12
|
+
function parseToml(
|
|
13
|
+
text: string,
|
|
14
|
+
filePath: string,
|
|
15
|
+
): { data: Record<string, unknown> } | { diagnostic: SourceDiagnostic } {
|
|
16
|
+
try {
|
|
17
|
+
const data = TOML.parse(text) as unknown;
|
|
18
|
+
if (typeof data !== "object" || data === null) {
|
|
19
|
+
return {
|
|
20
|
+
diagnostic: {
|
|
21
|
+
source: SOURCE,
|
|
22
|
+
type: "mcp",
|
|
23
|
+
status: "failed_invalid_source",
|
|
24
|
+
message: `${filePath} is not a TOML table`,
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return { data: data as Record<string, unknown> };
|
|
29
|
+
} catch (error) {
|
|
30
|
+
return {
|
|
31
|
+
diagnostic: {
|
|
32
|
+
source: SOURCE,
|
|
33
|
+
type: "mcp",
|
|
34
|
+
status: "failed_invalid_source",
|
|
35
|
+
message: `invalid TOML in ${filePath}: ${(error as Error).message}`,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const codexAdapter: Adapter = {
|
|
42
|
+
source: SOURCE,
|
|
43
|
+
async collect({ homeDir }: AdapterOptions): Promise<AdapterResult> {
|
|
44
|
+
const result: AdapterResult = { mcpCandidates: [], skillCandidates: [], diagnostics: [] };
|
|
45
|
+
|
|
46
|
+
const configPath = path.join(homeDir, ".codex", "config.toml");
|
|
47
|
+
const read = await readSourceText(configPath, SOURCE, "mcp");
|
|
48
|
+
if ("diagnostic" in read) {
|
|
49
|
+
result.diagnostics.push(read.diagnostic);
|
|
50
|
+
} else {
|
|
51
|
+
const parsed = parseToml(read.text, configPath);
|
|
52
|
+
if ("diagnostic" in parsed) {
|
|
53
|
+
result.diagnostics.push(parsed.diagnostic);
|
|
54
|
+
} else {
|
|
55
|
+
const servers = parsed.data.mcp_servers;
|
|
56
|
+
if (servers && typeof servers === "object" && !Array.isArray(servers)) {
|
|
57
|
+
for (const [name, raw] of Object.entries(servers as Record<string, unknown>)) {
|
|
58
|
+
result.mcpCandidates.push({ source: SOURCE, name, raw } satisfies McpCandidate);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const prompts = await collectMarkdownPrompts(path.join(homeDir, ".codex", "prompts"), SOURCE);
|
|
65
|
+
result.skillCandidates.push(...prompts.candidates);
|
|
66
|
+
result.diagnostics.push(...prompts.diagnostics);
|
|
67
|
+
|
|
68
|
+
return result;
|
|
69
|
+
},
|
|
70
|
+
};
|