@gajae-code/coding-agent 0.6.4 → 0.7.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.
Files changed (231) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/dist/types/async/job-manager.d.ts +3 -1
  3. package/dist/types/cli/daemon-cli.d.ts +25 -0
  4. package/dist/types/cli/migrate-cli.d.ts +20 -0
  5. package/dist/types/cli/notify-cli.d.ts +23 -0
  6. package/dist/types/cli/setup-cli.d.ts +20 -1
  7. package/dist/types/commands/daemon.d.ts +41 -0
  8. package/dist/types/commands/migrate.d.ts +33 -0
  9. package/dist/types/commands/notify.d.ts +41 -0
  10. package/dist/types/config/keybindings.d.ts +4 -0
  11. package/dist/types/config/model-profile-activation.d.ts +12 -0
  12. package/dist/types/config/model-profiles.d.ts +2 -1
  13. package/dist/types/config/model-registry.d.ts +3 -3
  14. package/dist/types/config/models-config-schema.d.ts +5 -0
  15. package/dist/types/config/settings-schema.d.ts +38 -0
  16. package/dist/types/coordinator/contract.d.ts +1 -1
  17. package/dist/types/daemon/builtin.d.ts +20 -0
  18. package/dist/types/daemon/control-types.d.ts +57 -0
  19. package/dist/types/daemon/runtime.d.ts +25 -0
  20. package/dist/types/extensibility/extensions/types.d.ts +8 -0
  21. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +2 -0
  22. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -2
  23. package/dist/types/gjc-runtime/goal-mode-request.d.ts +1 -1
  24. package/dist/types/gjc-runtime/session-layout.d.ts +59 -0
  25. package/dist/types/gjc-runtime/session-resolution.d.ts +47 -0
  26. package/dist/types/gjc-runtime/state-graph.d.ts +1 -1
  27. package/dist/types/gjc-runtime/state-runtime.d.ts +5 -4
  28. package/dist/types/gjc-runtime/state-schema.d.ts +2 -0
  29. package/dist/types/gjc-runtime/state-writer.d.ts +38 -7
  30. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +15 -0
  31. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +21 -4
  32. package/dist/types/gjc-runtime/workflow-command-ref.d.ts +1 -1
  33. package/dist/types/gjc-runtime/workflow-manifest.d.ts +1 -1
  34. package/dist/types/harness-control-plane/storage.d.ts +2 -1
  35. package/dist/types/hooks/skill-state.d.ts +12 -4
  36. package/dist/types/migrate/action-planner.d.ts +11 -0
  37. package/dist/types/migrate/adapters/claude-code.d.ts +2 -0
  38. package/dist/types/migrate/adapters/codex.d.ts +5 -0
  39. package/dist/types/migrate/adapters/index.d.ts +45 -0
  40. package/dist/types/migrate/adapters/opencode.d.ts +2 -0
  41. package/dist/types/migrate/executor.d.ts +2 -0
  42. package/dist/types/migrate/mcp-mapper.d.ts +20 -0
  43. package/dist/types/migrate/report.d.ts +18 -0
  44. package/dist/types/migrate/skill-normalizer.d.ts +27 -0
  45. package/dist/types/migrate/types.d.ts +126 -0
  46. package/dist/types/modes/components/custom-editor.d.ts +1 -1
  47. package/dist/types/modes/components/oauth-selector.d.ts +2 -0
  48. package/dist/types/modes/controllers/selector-controller.d.ts +2 -2
  49. package/dist/types/modes/interactive-mode.d.ts +1 -1
  50. package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +1 -1
  51. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
  52. package/dist/types/modes/types.d.ts +7 -1
  53. package/dist/types/notifications/config-commands.d.ts +26 -0
  54. package/dist/types/notifications/config.d.ts +61 -0
  55. package/dist/types/notifications/helpers.d.ts +55 -0
  56. package/dist/types/notifications/html-format.d.ts +62 -0
  57. package/dist/types/notifications/index.d.ts +28 -0
  58. package/dist/types/notifications/rate-limit-pool.d.ts +93 -0
  59. package/dist/types/notifications/telegram-cli.d.ts +19 -0
  60. package/dist/types/notifications/telegram-daemon-cli.d.ts +11 -0
  61. package/dist/types/notifications/telegram-daemon-control.d.ts +56 -0
  62. package/dist/types/notifications/telegram-daemon.d.ts +276 -0
  63. package/dist/types/notifications/telegram-reference.d.ts +111 -0
  64. package/dist/types/notifications/threaded-inbound.d.ts +58 -0
  65. package/dist/types/notifications/threaded-render.d.ts +66 -0
  66. package/dist/types/notifications/topic-registry.d.ts +67 -0
  67. package/dist/types/research-plan/index.d.ts +1 -0
  68. package/dist/types/research-plan/ledger.d.ts +33 -0
  69. package/dist/types/rlm/artifacts.d.ts +1 -1
  70. package/dist/types/rlm/index.d.ts +12 -0
  71. package/dist/types/runtime-mcp/config-writer.d.ts +26 -0
  72. package/dist/types/session/agent-session.d.ts +39 -2
  73. package/dist/types/session/auth-storage.d.ts +1 -1
  74. package/dist/types/setup/credential-auto-import.d.ts +63 -0
  75. package/dist/types/setup/credential-import.d.ts +3 -0
  76. package/dist/types/setup/host-plugin-setup.d.ts +39 -0
  77. package/dist/types/skill-state/active-state.d.ts +6 -11
  78. package/dist/types/skill-state/canonical-skills.d.ts +3 -0
  79. package/dist/types/skill-state/workflow-hud.d.ts +2 -0
  80. package/dist/types/task/spawn-gate.d.ts +1 -10
  81. package/dist/types/tools/ask-answer-registry.d.ts +13 -0
  82. package/dist/types/tools/index.d.ts +18 -0
  83. package/dist/types/tools/subagent.d.ts +3 -0
  84. package/package.json +7 -7
  85. package/scripts/build-binary.ts +3 -0
  86. package/src/async/job-manager.ts +5 -1
  87. package/src/cli/daemon-cli.ts +122 -0
  88. package/src/cli/migrate-cli.ts +106 -0
  89. package/src/cli/notify-cli.ts +274 -0
  90. package/src/cli/setup-cli.ts +173 -84
  91. package/src/cli.ts +3 -0
  92. package/src/commands/daemon.ts +47 -0
  93. package/src/commands/deep-interview.ts +2 -2
  94. package/src/commands/migrate.ts +46 -0
  95. package/src/commands/notify.ts +61 -0
  96. package/src/commands/setup.ts +11 -1
  97. package/src/commands/state.ts +2 -1
  98. package/src/commands/team.ts +7 -3
  99. package/src/config/model-profile-activation.ts +74 -5
  100. package/src/config/model-profiles.ts +7 -4
  101. package/src/config/model-registry.ts +6 -3
  102. package/src/config/models-config-schema.ts +1 -1
  103. package/src/config/settings-schema.ts +29 -0
  104. package/src/coordinator/contract.ts +3 -0
  105. package/src/coordinator-mcp/policy.ts +10 -2
  106. package/src/coordinator-mcp/server.ts +270 -1
  107. package/src/daemon/builtin.ts +46 -0
  108. package/src/daemon/control-types.ts +65 -0
  109. package/src/daemon/runtime.ts +51 -0
  110. package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +0 -1
  111. package/src/defaults/gjc/skills/deep-interview/SKILL.md +28 -24
  112. package/src/defaults/gjc/skills/ralplan/SKILL.md +8 -4
  113. package/src/defaults/gjc/skills/team/SKILL.md +51 -47
  114. package/src/defaults/gjc/skills/ultragoal/SKILL.md +33 -13
  115. package/src/extensibility/custom-commands/loader.ts +0 -7
  116. package/src/extensibility/extensions/runner.ts +4 -0
  117. package/src/extensibility/extensions/types.ts +8 -0
  118. package/src/extensibility/gjc-plugins/injection.ts +23 -4
  119. package/src/extensibility/gjc-plugins/state.ts +16 -1
  120. package/src/gjc-runtime/deep-interview-recorder.ts +51 -18
  121. package/src/gjc-runtime/deep-interview-runtime.ts +49 -23
  122. package/src/gjc-runtime/goal-mode-request.ts +26 -11
  123. package/src/gjc-runtime/launch-tmux.ts +6 -1
  124. package/src/gjc-runtime/ralplan-runtime.ts +79 -50
  125. package/src/gjc-runtime/session-layout.ts +180 -0
  126. package/src/gjc-runtime/session-resolution.ts +217 -0
  127. package/src/gjc-runtime/state-graph.ts +1 -2
  128. package/src/gjc-runtime/state-migrations.ts +1 -0
  129. package/src/gjc-runtime/state-runtime.ts +247 -124
  130. package/src/gjc-runtime/state-schema.ts +2 -0
  131. package/src/gjc-runtime/state-writer.ts +289 -41
  132. package/src/gjc-runtime/team-runtime.ts +43 -19
  133. package/src/gjc-runtime/tmux-sessions.ts +7 -1
  134. package/src/gjc-runtime/ultragoal-guard.ts +102 -4
  135. package/src/gjc-runtime/ultragoal-runtime.ts +226 -60
  136. package/src/gjc-runtime/workflow-command-ref.ts +1 -2
  137. package/src/gjc-runtime/workflow-manifest.generated.json +27 -2
  138. package/src/gjc-runtime/workflow-manifest.ts +12 -3
  139. package/src/goals/tools/goal-tool.ts +11 -2
  140. package/src/harness-control-plane/storage.ts +14 -4
  141. package/src/hooks/native-skill-hook.ts +38 -12
  142. package/src/hooks/skill-state.ts +178 -83
  143. package/src/internal-urls/docs-index.generated.ts +9 -6
  144. package/src/main.ts +30 -0
  145. package/src/migrate/action-planner.ts +318 -0
  146. package/src/migrate/adapters/claude-code.ts +39 -0
  147. package/src/migrate/adapters/codex.ts +70 -0
  148. package/src/migrate/adapters/index.ts +277 -0
  149. package/src/migrate/adapters/opencode.ts +52 -0
  150. package/src/migrate/executor.ts +81 -0
  151. package/src/migrate/mcp-mapper.ts +152 -0
  152. package/src/migrate/report.ts +104 -0
  153. package/src/migrate/skill-normalizer.ts +80 -0
  154. package/src/migrate/types.ts +163 -0
  155. package/src/modes/acp/acp-event-mapper.ts +1 -0
  156. package/src/modes/bridge/bridge-mode.ts +2 -2
  157. package/src/modes/components/custom-editor.ts +30 -20
  158. package/src/modes/components/hook-editor.ts +7 -2
  159. package/src/modes/components/oauth-selector.ts +19 -0
  160. package/src/modes/controllers/event-controller.ts +20 -0
  161. package/src/modes/controllers/selector-controller.ts +80 -17
  162. package/src/modes/interactive-mode.ts +6 -2
  163. package/src/modes/rpc/rpc-mode.ts +2 -2
  164. package/src/modes/runtime-init.ts +1 -0
  165. package/src/modes/shared/agent-wire/event-contract.ts +1 -0
  166. package/src/modes/shared/agent-wire/event-envelope.ts +1 -0
  167. package/src/modes/shared/agent-wire/event-observation.ts +16 -0
  168. package/src/modes/shared/agent-wire/unattended-audit.ts +3 -2
  169. package/src/modes/shared/agent-wire/unattended-session.ts +22 -0
  170. package/src/modes/types.ts +7 -1
  171. package/src/modes/utils/ui-helpers.ts +23 -0
  172. package/src/notifications/config-commands.ts +50 -0
  173. package/src/notifications/config.ts +107 -0
  174. package/src/notifications/helpers.ts +135 -0
  175. package/src/notifications/html-format.ts +389 -0
  176. package/src/notifications/index.ts +663 -0
  177. package/src/notifications/rate-limit-pool.ts +179 -0
  178. package/src/notifications/telegram-cli.ts +194 -0
  179. package/src/notifications/telegram-daemon-cli.ts +74 -0
  180. package/src/notifications/telegram-daemon-control.ts +370 -0
  181. package/src/notifications/telegram-daemon.ts +1370 -0
  182. package/src/notifications/telegram-reference.ts +335 -0
  183. package/src/notifications/threaded-inbound.ts +80 -0
  184. package/src/notifications/threaded-render.ts +155 -0
  185. package/src/notifications/topic-registry.ts +133 -0
  186. package/src/prompts/agents/init.md +1 -1
  187. package/src/prompts/system/plan-mode-active.md +1 -1
  188. package/src/prompts/tools/ast-grep.md +1 -1
  189. package/src/prompts/tools/search.md +1 -1
  190. package/src/prompts/tools/task.md +1 -2
  191. package/src/research-plan/index.ts +1 -0
  192. package/src/research-plan/ledger.ts +177 -0
  193. package/src/rlm/artifacts.ts +12 -3
  194. package/src/rlm/index.ts +26 -0
  195. package/src/runtime-mcp/config-writer.ts +46 -0
  196. package/src/sdk.ts +16 -0
  197. package/src/session/agent-session.ts +128 -24
  198. package/src/session/auth-storage.ts +3 -0
  199. package/src/session/session-dump-format.ts +43 -2
  200. package/src/session/session-manager.ts +39 -5
  201. package/src/setup/credential-auto-import.ts +258 -0
  202. package/src/setup/credential-import.ts +17 -0
  203. package/src/setup/hermes/templates/operator-instructions.v1.md +10 -0
  204. package/src/setup/hermes-setup.ts +1 -1
  205. package/src/setup/host-plugin-setup.ts +142 -0
  206. package/src/skill-state/active-state.ts +72 -108
  207. package/src/skill-state/canonical-skills.ts +4 -0
  208. package/src/skill-state/deep-interview-mutation-guard.ts +28 -109
  209. package/src/skill-state/workflow-hud.ts +4 -2
  210. package/src/skill-state/workflow-state-contract.ts +3 -3
  211. package/src/slash-commands/builtin-registry.ts +4 -1
  212. package/src/task/agents.ts +1 -22
  213. package/src/task/executor.ts +5 -1
  214. package/src/task/index.ts +1 -41
  215. package/src/task/spawn-gate.ts +1 -38
  216. package/src/task/types.ts +1 -1
  217. package/src/tools/ask-answer-registry.ts +25 -0
  218. package/src/tools/ask.ts +108 -16
  219. package/src/tools/computer.ts +58 -4
  220. package/src/tools/image-gen.ts +5 -8
  221. package/src/tools/index.ts +19 -0
  222. package/src/tools/inspect-image.ts +16 -11
  223. package/src/tools/subagent-render.ts +7 -0
  224. package/src/tools/subagent.ts +38 -7
  225. package/dist/types/extensibility/custom-commands/bundled/review/index.d.ts +0 -10
  226. package/src/extensibility/custom-commands/bundled/review/index.ts +0 -456
  227. package/src/prompts/agents/explore.md +0 -58
  228. package/src/prompts/agents/plan.md +0 -49
  229. package/src/prompts/agents/reviewer.md +0 -141
  230. package/src/prompts/agents/task.md +0 -16
  231. package/src/prompts/review-request.md +0 -70
package/src/main.ts CHANGED
@@ -47,6 +47,7 @@ import {
47
47
  import type { AgentSession } from "./session/agent-session";
48
48
  import type { AuthStorage } from "./session/auth-storage";
49
49
  import { resolveResumableSession, type SessionInfo, SessionManager } from "./session/session-manager";
50
+ import { runStartupCredentialAutoImportIfNeeded } from "./setup/credential-auto-import";
50
51
  import { formatModelOnboardingGuidance } from "./setup/model-onboarding-guidance";
51
52
  import { executeBuiltinSlashCommand } from "./slash-commands/builtin-registry";
52
53
  import { resolvePromptInput } from "./system-prompt";
@@ -856,6 +857,14 @@ export async function runRootCommand(
856
857
  settingsInstance.get("theme.light"),
857
858
  );
858
859
 
860
+ const credentialAutoImportNotice = isInteractive
861
+ ? await logger.time("credentialAutoImport", runStartupCredentialAutoImportIfNeeded, {
862
+ authStorage,
863
+ modelRegistry,
864
+ agentDir: settingsInstance.getAgentDir(),
865
+ })
866
+ : undefined;
867
+
859
868
  let scopedModels: ScopedModel[] = [];
860
869
  const modelPatterns = parsedArgs.models ?? settingsInstance.get("enabledModels");
861
870
  const modelMatchPreferences = {
@@ -895,6 +904,24 @@ export async function runRootCommand(
895
904
  sessionManager = await SessionManager.open(selectedPath);
896
905
  }
897
906
 
907
+ // Restore the resumed session's working directory so the HUD branch, the
908
+ // project path, and the agent's tools all match where the session was
909
+ // created. A `--worktree` session lives in a linked worktree whose path
910
+ // differs from where `--continue`/`--resume` is invoked, which would
911
+ // otherwise leave the HUD pinned to the main checkout's branch.
912
+ if (sessionManager && !parsedArgs.cwd) {
913
+ const sessionCwd = sessionManager.getCwd();
914
+ if (sessionCwd && normalizePathForComparison(sessionCwd) !== normalizePathForComparison(getProjectDir())) {
915
+ try {
916
+ if ((await fs.stat(sessionCwd)).isDirectory()) {
917
+ setProjectDir(sessionCwd);
918
+ }
919
+ } catch {
920
+ // Session cwd no longer exists (e.g. worktree removed); keep current dir.
921
+ }
922
+ }
923
+ }
924
+
898
925
  const { options: sessionOptions } = await logger.time(
899
926
  "buildSessionOptions",
900
927
  buildSessionOptions,
@@ -976,6 +1003,9 @@ export async function runRootCommand(
976
1003
  if (modelRegistryError) {
977
1004
  notifs.push({ kind: "error", message: modelRegistryError.message });
978
1005
  }
1006
+ if (credentialAutoImportNotice) {
1007
+ notifs.push({ kind: "info", message: credentialAutoImportNotice });
1008
+ }
979
1009
 
980
1010
  if (isInteractive && !session.model && !modelFallbackMessage) {
981
1011
  notifs.push({
@@ -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 "@gajae-code/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
+ };