@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
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Source adapters for `gjc migrate`.
3
+ *
4
+ * Each adapter reads GLOBAL/home config for one source agent and returns
5
+ * normalized MCP + skill candidates plus source-level diagnostics. Adapters never
6
+ * read project-level config and never connect to anything.
7
+ */
8
+ import * as fs from "node:fs/promises";
9
+ import { isEnoent } from "@gajae-code/utils";
10
+ import { normalizeSkill } from "../skill-normalizer";
11
+ import type { AdapterResult, MigrateSource, SkillCandidate, SourceDiagnostic } from "../types";
12
+ import { claudeCodeAdapter } from "./claude-code";
13
+ import { codexAdapter } from "./codex";
14
+ import { opencodeAdapter } from "./opencode";
15
+
16
+ export interface AdapterOptions {
17
+ /** Home directory root; overridable for tests. */
18
+ homeDir: string;
19
+ }
20
+
21
+ export interface Adapter {
22
+ source: MigrateSource;
23
+ collect(options: AdapterOptions): Promise<AdapterResult>;
24
+ }
25
+
26
+ const ADAPTERS: Record<MigrateSource, Adapter> = {
27
+ "claude-code": claudeCodeAdapter,
28
+ codex: codexAdapter,
29
+ opencode: opencodeAdapter,
30
+ };
31
+
32
+ export function getAdapter(source: MigrateSource): Adapter {
33
+ return ADAPTERS[source];
34
+ }
35
+
36
+ /** Read a text file, classifying absence/IO errors into source diagnostics. */
37
+ export async function readSourceText(
38
+ filePath: string,
39
+ source: MigrateSource,
40
+ type: SourceDiagnostic["type"],
41
+ ): Promise<{ text: string } | { diagnostic: SourceDiagnostic }> {
42
+ try {
43
+ return { text: await fs.readFile(filePath, "utf-8") };
44
+ } catch (error) {
45
+ if (isEnoent(error)) {
46
+ return {
47
+ diagnostic: { source, type, status: "skipped_absent_source", message: `no ${type} config at ${filePath}` },
48
+ };
49
+ }
50
+ return {
51
+ diagnostic: {
52
+ source,
53
+ type,
54
+ status: "failed_io",
55
+ message: `failed to read ${filePath}: ${(error as Error).message}`,
56
+ },
57
+ };
58
+ }
59
+ }
60
+
61
+ /** Parse JSON text, classifying parse errors into a `failed_invalid_source` diagnostic. */
62
+ export function parseSourceJson(
63
+ text: string,
64
+ filePath: string,
65
+ source: MigrateSource,
66
+ type: SourceDiagnostic["type"],
67
+ ): { data: Record<string, unknown> } | { diagnostic: SourceDiagnostic } {
68
+ try {
69
+ const data = JSON.parse(text) as unknown;
70
+ if (typeof data !== "object" || data === null) {
71
+ return {
72
+ diagnostic: { source, type, status: "failed_invalid_source", message: `${filePath} is not a JSON object` },
73
+ };
74
+ }
75
+ return { data: data as Record<string, unknown> };
76
+ } catch (error) {
77
+ return {
78
+ diagnostic: {
79
+ source,
80
+ type,
81
+ status: "failed_invalid_source",
82
+ message: `invalid JSON in ${filePath}: ${(error as Error).message}`,
83
+ },
84
+ };
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Collect skill candidates from a directory of `<name>/SKILL.md` entries.
90
+ * A missing directory yields a `skipped_absent_source` diagnostic.
91
+ */
92
+ export async function collectSkillDir(
93
+ dir: string,
94
+ source: MigrateSource,
95
+ ): Promise<{ candidates: SkillCandidate[]; diagnostics: SourceDiagnostic[] }> {
96
+ const candidates: SkillCandidate[] = [];
97
+ const diagnostics: SourceDiagnostic[] = [];
98
+ let entries: string[];
99
+ try {
100
+ const dirents = await fs.readdir(dir, { withFileTypes: true });
101
+ entries = dirents.filter(d => d.isDirectory()).map(d => d.name);
102
+ } catch (error) {
103
+ if (isEnoent(error)) {
104
+ diagnostics.push({
105
+ source,
106
+ type: "skill",
107
+ status: "skipped_absent_source",
108
+ message: `no skills dir at ${dir}`,
109
+ });
110
+ } else {
111
+ diagnostics.push({
112
+ source,
113
+ type: "skill",
114
+ status: "failed_io",
115
+ message: `failed to read ${dir}: ${(error as Error).message}`,
116
+ });
117
+ }
118
+ return { candidates, diagnostics };
119
+ }
120
+
121
+ for (const name of entries.sort()) {
122
+ const skillFile = `${dir}/${name}/SKILL.md`;
123
+ const read = await readSourceText(skillFile, source, "skill");
124
+ if ("diagnostic" in read) {
125
+ // A subdir without SKILL.md is simply not a skill; only surface non-absent errors.
126
+ if (read.diagnostic.status !== "skipped_absent_source") diagnostics.push(read.diagnostic);
127
+ continue;
128
+ }
129
+ try {
130
+ const normalized = normalizeSkill({ rawName: name, content: read.text });
131
+ candidates.push({ source, slug: normalized.slug, content: normalized.content, warnings: normalized.warnings });
132
+ } catch (error) {
133
+ diagnostics.push({
134
+ source,
135
+ type: "skill",
136
+ status: "failed_invalid_source",
137
+ message: `failed to normalize skill ${skillFile}: ${(error as Error).message}`,
138
+ });
139
+ }
140
+ }
141
+ return { candidates, diagnostics };
142
+ }
143
+
144
+ /**
145
+ * Collect skill candidates from a flat directory of `*.md` prompt/command files.
146
+ */
147
+ export async function collectMarkdownPrompts(
148
+ dir: string,
149
+ source: MigrateSource,
150
+ ): Promise<{ candidates: SkillCandidate[]; diagnostics: SourceDiagnostic[] }> {
151
+ const candidates: SkillCandidate[] = [];
152
+ const diagnostics: SourceDiagnostic[] = [];
153
+ let files: string[];
154
+ try {
155
+ const dirents = await fs.readdir(dir, { withFileTypes: true });
156
+ files = dirents.filter(d => d.isFile() && d.name.endsWith(".md")).map(d => d.name);
157
+ } catch (error) {
158
+ if (isEnoent(error)) {
159
+ diagnostics.push({
160
+ source,
161
+ type: "skill",
162
+ status: "skipped_absent_source",
163
+ message: `no prompts dir at ${dir}`,
164
+ });
165
+ } else {
166
+ diagnostics.push({
167
+ source,
168
+ type: "skill",
169
+ status: "failed_io",
170
+ message: `failed to read ${dir}: ${(error as Error).message}`,
171
+ });
172
+ }
173
+ return { candidates, diagnostics };
174
+ }
175
+
176
+ for (const file of files.sort()) {
177
+ const promptFile = `${dir}/${file}`;
178
+ const read = await readSourceText(promptFile, source, "skill");
179
+ if ("diagnostic" in read) {
180
+ diagnostics.push(read.diagnostic);
181
+ continue;
182
+ }
183
+ const rawName = file.replace(/\.md$/, "");
184
+ try {
185
+ const normalized = normalizeSkill({ rawName, content: read.text });
186
+ candidates.push({ source, slug: normalized.slug, content: normalized.content, warnings: normalized.warnings });
187
+ } catch (error) {
188
+ diagnostics.push({
189
+ source,
190
+ type: "skill",
191
+ status: "failed_invalid_source",
192
+ message: `failed to convert prompt ${promptFile}: ${(error as Error).message}`,
193
+ });
194
+ }
195
+ }
196
+ return { candidates, diagnostics };
197
+ }
198
+
199
+ /**
200
+ * Recursively collect skill candidates from any `**​/SKILL.md` under `root`.
201
+ * The slug derives from the directory that directly contains the `SKILL.md`.
202
+ */
203
+ export async function collectSkillTree(
204
+ root: string,
205
+ source: MigrateSource,
206
+ ): Promise<{ candidates: SkillCandidate[]; diagnostics: SourceDiagnostic[] }> {
207
+ const candidates: SkillCandidate[] = [];
208
+ const diagnostics: SourceDiagnostic[] = [];
209
+
210
+ async function walk(dir: string): Promise<void> {
211
+ const entries = await fs.readdir(dir, { withFileTypes: true }).catch((error: unknown) => {
212
+ if (isEnoent(error)) return null;
213
+ diagnostics.push({
214
+ source,
215
+ type: "skill",
216
+ status: "failed_io",
217
+ message: `failed to read ${dir}: ${(error as Error).message}`,
218
+ });
219
+ return null;
220
+ });
221
+ if (!entries) return;
222
+
223
+ const hasSkill = entries.some(e => e.isFile() && String(e.name) === "SKILL.md");
224
+ if (hasSkill) {
225
+ const skillFile = `${dir}/SKILL.md`;
226
+ const read = await readSourceText(skillFile, source, "skill");
227
+ if ("diagnostic" in read) {
228
+ if (read.diagnostic.status !== "skipped_absent_source") diagnostics.push(read.diagnostic);
229
+ } else {
230
+ try {
231
+ const rawName = dir.split("/").pop() ?? dir;
232
+ const normalized = normalizeSkill({ rawName, content: read.text });
233
+ candidates.push({
234
+ source,
235
+ slug: normalized.slug,
236
+ content: normalized.content,
237
+ warnings: normalized.warnings,
238
+ });
239
+ } catch (error) {
240
+ diagnostics.push({
241
+ source,
242
+ type: "skill",
243
+ status: "failed_invalid_source",
244
+ message: `failed to normalize skill ${skillFile}: ${(error as Error).message}`,
245
+ });
246
+ }
247
+ }
248
+ }
249
+
250
+ for (const entry of entries) {
251
+ if (entry.isDirectory()) await walk(`${dir}/${String(entry.name)}`);
252
+ }
253
+ }
254
+
255
+ // Surface an absent root the same way the flat collectors do.
256
+ const rootEntries = await fs.readdir(root).catch((error: unknown) => {
257
+ if (isEnoent(error)) {
258
+ diagnostics.push({
259
+ source,
260
+ type: "skill",
261
+ status: "skipped_absent_source",
262
+ message: `no skills dir at ${root}`,
263
+ });
264
+ } else {
265
+ diagnostics.push({
266
+ source,
267
+ type: "skill",
268
+ status: "failed_io",
269
+ message: `failed to read ${root}: ${(error as Error).message}`,
270
+ });
271
+ }
272
+ return null;
273
+ });
274
+ if (rootEntries) await walk(root);
275
+
276
+ return { candidates, diagnostics };
277
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * OpenCode adapter: reads `~/.config/opencode/opencode.json` (mcp), plus
3
+ * `~/.config/opencode/skills` and `~/.config/opencode/commands`.
4
+ */
5
+ import * as path from "node:path";
6
+ import type { AdapterResult, McpCandidate } from "../types";
7
+ import {
8
+ type Adapter,
9
+ type AdapterOptions,
10
+ collectMarkdownPrompts,
11
+ collectSkillTree,
12
+ parseSourceJson,
13
+ readSourceText,
14
+ } from "./index";
15
+
16
+ const SOURCE = "opencode" as const;
17
+
18
+ export const opencodeAdapter: Adapter = {
19
+ source: SOURCE,
20
+ async collect({ homeDir }: AdapterOptions): Promise<AdapterResult> {
21
+ const result: AdapterResult = { mcpCandidates: [], skillCandidates: [], diagnostics: [] };
22
+ const baseDir = path.join(homeDir, ".config", "opencode");
23
+
24
+ const configPath = path.join(baseDir, "opencode.json");
25
+ const read = await readSourceText(configPath, SOURCE, "mcp");
26
+ if ("diagnostic" in read) {
27
+ result.diagnostics.push(read.diagnostic);
28
+ } else {
29
+ const parsed = parseSourceJson(read.text, configPath, SOURCE, "mcp");
30
+ if ("diagnostic" in parsed) {
31
+ result.diagnostics.push(parsed.diagnostic);
32
+ } else {
33
+ const servers = parsed.data.mcp;
34
+ if (servers && typeof servers === "object" && !Array.isArray(servers)) {
35
+ for (const [name, raw] of Object.entries(servers as Record<string, unknown>)) {
36
+ result.mcpCandidates.push({ source: SOURCE, name, raw } satisfies McpCandidate);
37
+ }
38
+ }
39
+ }
40
+ }
41
+
42
+ const skills = await collectSkillTree(path.join(baseDir, "skills"), SOURCE);
43
+ result.skillCandidates.push(...skills.candidates);
44
+ result.diagnostics.push(...skills.diagnostics);
45
+
46
+ const commands = await collectMarkdownPrompts(path.join(baseDir, "commands"), SOURCE);
47
+ result.skillCandidates.push(...commands.candidates);
48
+ result.diagnostics.push(...commands.diagnostics);
49
+
50
+ return result;
51
+ },
52
+ };
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Execute planned migration actions.
3
+ *
4
+ * Consumes the planner's actions unchanged and performs only `create`/`update`
5
+ * operations. It never re-plans. Writes are not transactional: on a write error
6
+ * the offending action flips to `failed_io`, already-written actions remain, and
7
+ * remaining actions still run (no rollback). Dry-run never calls this.
8
+ */
9
+ import * as fsSync from "node:fs";
10
+ import * as fs from "node:fs/promises";
11
+ import * as path from "node:path";
12
+ import { upsertMCPServer } from "../runtime-mcp/config-writer";
13
+ import type { MigrateAction } from "./types";
14
+
15
+ async function ensureRealDirectoryPathNoFollow(directory: string): Promise<void> {
16
+ const resolved = path.resolve(directory);
17
+ const parsed = path.parse(resolved);
18
+ let current = parsed.root;
19
+ for (const part of resolved.slice(parsed.root.length).split(path.sep).filter(Boolean)) {
20
+ current = path.join(current, part);
21
+ try {
22
+ const stat = await fs.lstat(current);
23
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
24
+ throw new Error(`skill destination ancestor is not a real directory: ${current}`);
25
+ }
26
+ } catch (error) {
27
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
28
+ await fs.mkdir(current);
29
+ const stat = await fs.lstat(current);
30
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
31
+ throw new Error(`skill destination ancestor is not a real directory: ${current}`);
32
+ }
33
+ }
34
+ }
35
+ }
36
+
37
+ async function writeSkillFileNoFollow(destination: string, content: string): Promise<void> {
38
+ const skillDir = path.dirname(destination);
39
+ await ensureRealDirectoryPathNoFollow(skillDir);
40
+ const handle = await fs.open(
41
+ destination,
42
+ fsSync.constants.O_WRONLY | fsSync.constants.O_CREAT | fsSync.constants.O_TRUNC | fsSync.constants.O_NOFOLLOW,
43
+ 0o666,
44
+ );
45
+ try {
46
+ await handle.writeFile(content, "utf-8");
47
+ } finally {
48
+ await handle.close();
49
+ }
50
+ }
51
+
52
+ export async function executeActions(actions: MigrateAction[]): Promise<MigrateAction[]> {
53
+ const out: MigrateAction[] = [];
54
+ for (const action of actions) {
55
+ if (action.operation !== "create" && action.operation !== "update") {
56
+ out.push(action);
57
+ continue;
58
+ }
59
+ try {
60
+ if (action.type === "mcp" && action.mcp && action.name && action.destination) {
61
+ await upsertMCPServer(action.destination, action.name, action.mcp.config, {
62
+ force: action.operation === "update",
63
+ });
64
+ out.push(action);
65
+ } else if (action.type === "skill" && action.skill && action.destination) {
66
+ await writeSkillFileNoFollow(action.destination, action.skill.content);
67
+ out.push(action);
68
+ } else {
69
+ out.push(action);
70
+ }
71
+ } catch (error) {
72
+ out.push({
73
+ ...action,
74
+ operation: "fail",
75
+ status: "failed_io",
76
+ reason: `write failed: ${(error as Error).message}`,
77
+ });
78
+ }
79
+ }
80
+ return out;
81
+ }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Map raw source MCP server entries onto GJC `MCPServerConfig`.
3
+ *
4
+ * Implements the source-schema compatibility matrix from the consensus plan:
5
+ * preserved (P), transformed (T), omitted-with-warning (OW), skipped (S,
6
+ * `skipped_unmappable`), and failed (F, `failed_invalid_source`). Secret-indirection
7
+ * fields are always omitted-with-warning; their values are never read or emitted.
8
+ */
9
+ import type { MCPServerConfig } from "../runtime-mcp/types";
10
+ import type { MigrateSource } from "./types";
11
+
12
+ export type McpMapOutcome =
13
+ | { ok: true; config: MCPServerConfig; warnings: string[] }
14
+ | { ok: false; status: "skipped_unmappable" | "failed_invalid_source"; reason: string };
15
+
16
+ function isRecord(value: unknown): value is Record<string, unknown> {
17
+ return typeof value === "object" && value !== null && !Array.isArray(value);
18
+ }
19
+
20
+ function asStringArray(value: unknown): string[] | undefined | "invalid" {
21
+ if (value === undefined) return undefined;
22
+ if (!Array.isArray(value) || !value.every(v => typeof v === "string")) return "invalid";
23
+ return value as string[];
24
+ }
25
+
26
+ function asStringRecord(value: unknown): Record<string, string> | undefined | "invalid" {
27
+ if (value === undefined) return undefined;
28
+ if (!isRecord(value) || !Object.values(value).every(v => typeof v === "string")) return "invalid";
29
+ return value as Record<string, string>;
30
+ }
31
+
32
+ /** Secret-indirection fields per source: always omitted-with-warning, never read. */
33
+ const SECRET_INDIRECTION_FIELDS: Record<MigrateSource, string[]> = {
34
+ "claude-code": [],
35
+ codex: ["env_vars", "env_http_headers", "bearer_token_env_var"],
36
+ opencode: [],
37
+ };
38
+
39
+ /** Fields recognized for a source (handled or intentionally omitted). Anything else is omitted-with-warning. */
40
+ const RECOGNIZED_FIELDS: Record<MigrateSource, ReadonlySet<string>> = {
41
+ "claude-code": new Set(["type", "command", "args", "env", "url", "headers", "enabled", "timeout", "cwd"]),
42
+ codex: new Set([
43
+ "type",
44
+ "command",
45
+ "args",
46
+ "env",
47
+ "url",
48
+ "http_headers",
49
+ "cwd",
50
+ "enabled",
51
+ "timeout",
52
+ "tool_timeout_sec",
53
+ // omitted-with-warning fields are still "recognized" (handled below):
54
+ "env_vars",
55
+ "env_http_headers",
56
+ "bearer_token_env_var",
57
+ "startup_timeout_sec",
58
+ "enabled_tools",
59
+ "disabled_tools",
60
+ ]),
61
+ opencode: new Set(["type", "command", "args", "env", "url", "headers", "enabled", "timeout", "cwd"]),
62
+ };
63
+
64
+ /** Fields with no GJC equivalent: omitted-with-warning (named explicitly so the warning is precise). */
65
+ const OMITTED_FIELDS: Record<MigrateSource, string[]> = {
66
+ "claude-code": [],
67
+ codex: ["startup_timeout_sec", "enabled_tools", "disabled_tools"],
68
+ opencode: [],
69
+ };
70
+
71
+ export function mapMcpEntry(source: MigrateSource, name: string, raw: unknown): McpMapOutcome {
72
+ if (!isRecord(raw)) {
73
+ return { ok: false, status: "failed_invalid_source", reason: `server "${name}" is not an object` };
74
+ }
75
+
76
+ const warnings: string[] = [];
77
+ for (const field of SECRET_INDIRECTION_FIELDS[source]) {
78
+ if (field in raw) warnings.push(`omitted secret-indirection field "${field}" for "${name}" (value not read)`);
79
+ }
80
+ for (const field of OMITTED_FIELDS[source]) {
81
+ if (field in raw) warnings.push(`omitted unsupported field "${field}" for "${name}"`);
82
+ }
83
+ // Unknown/unrecognized fields: omit-with-warning (never copy their values).
84
+ for (const field of Object.keys(raw)) {
85
+ if (!RECOGNIZED_FIELDS[source].has(field)) {
86
+ warnings.push(`omitted unknown field "${field}" for "${name}"`);
87
+ }
88
+ }
89
+
90
+ const rawType = typeof raw.type === "string" ? raw.type : undefined;
91
+ const command = typeof raw.command === "string" ? raw.command : undefined;
92
+ const url = typeof raw.url === "string" ? raw.url : undefined;
93
+
94
+ const base: { enabled?: boolean; timeout?: number } = {};
95
+ if (typeof raw.enabled === "boolean") base.enabled = raw.enabled;
96
+ if (typeof raw.timeout === "number") base.timeout = raw.timeout;
97
+ // Codex tool_timeout_sec -> timeout (ms).
98
+ if (source === "codex" && typeof raw.tool_timeout_sec === "number") {
99
+ base.timeout = raw.tool_timeout_sec * 1000;
100
+ }
101
+
102
+ const wantsStdio =
103
+ source === "opencode" ? rawType === "local" || (!rawType && !!command) : !!command || rawType === "stdio";
104
+ const wantsHttp =
105
+ source === "opencode"
106
+ ? rawType === "remote" || rawType === "sse"
107
+ : rawType === "http" || rawType === "sse" || (!command && !!url);
108
+
109
+ if (wantsStdio) {
110
+ if (!command) {
111
+ return {
112
+ ok: false,
113
+ status: "skipped_unmappable",
114
+ reason: `server "${name}" has no command for stdio transport`,
115
+ };
116
+ }
117
+ const args = asStringArray(raw.args);
118
+ if (args === "invalid") {
119
+ return { ok: false, status: "failed_invalid_source", reason: `server "${name}" has invalid "args"` };
120
+ }
121
+ const env = asStringRecord(raw.env);
122
+ if (env === "invalid") {
123
+ return { ok: false, status: "failed_invalid_source", reason: `server "${name}" has invalid "env"` };
124
+ }
125
+ const config: MCPServerConfig = { type: "stdio", command, ...base };
126
+ if (args) config.args = args;
127
+ if (env) config.env = env;
128
+ if (typeof raw.cwd === "string") config.cwd = raw.cwd;
129
+ return { ok: true, config, warnings };
130
+ }
131
+
132
+ if (wantsHttp) {
133
+ if (!url) {
134
+ return {
135
+ ok: false,
136
+ status: "skipped_unmappable",
137
+ reason: `server "${name}" has no url for http/sse transport`,
138
+ };
139
+ }
140
+ const headerSource = source === "codex" ? raw.http_headers : raw.headers;
141
+ const headers = asStringRecord(headerSource);
142
+ if (headers === "invalid") {
143
+ return { ok: false, status: "failed_invalid_source", reason: `server "${name}" has invalid headers` };
144
+ }
145
+ const type = rawType === "sse" ? "sse" : "http";
146
+ const config = { type, url, ...base } as MCPServerConfig;
147
+ if (headers) (config as { headers?: Record<string, string> }).headers = headers;
148
+ return { ok: true, config, warnings };
149
+ }
150
+
151
+ return { ok: false, status: "skipped_unmappable", reason: `server "${name}" has neither a usable command nor url` };
152
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Build the human-readable and `--json` reports for `gjc migrate`.
3
+ *
4
+ * Secret values are never read upstream, so the report only ever contains field
5
+ * names in warnings — but rendering still treats action/warning text as opaque.
6
+ */
7
+ import {
8
+ emptyStatusCounts,
9
+ isFailureStatus,
10
+ type MigrateAction,
11
+ type MigrateDestinations,
12
+ type MigrateReport,
13
+ type MigrateSource,
14
+ type MigrateWarning,
15
+ type StatusCounts,
16
+ } from "./types";
17
+
18
+ export interface BuildReportInput {
19
+ actions: MigrateAction[];
20
+ warnings: MigrateWarning[];
21
+ sources: MigrateSource[];
22
+ destinations: MigrateDestinations;
23
+ dryRun: boolean;
24
+ project: boolean;
25
+ force: boolean;
26
+ }
27
+
28
+ export function buildReport(input: BuildReportInput): MigrateReport {
29
+ const total = emptyStatusCounts();
30
+ const byType = { mcp: emptyStatusCounts(), skill: emptyStatusCounts(), source: emptyStatusCounts() };
31
+ const bySource = {} as Record<MigrateSource, StatusCounts>;
32
+ for (const source of input.sources) bySource[source] = emptyStatusCounts();
33
+
34
+ let ok = true;
35
+ for (const action of input.actions) {
36
+ total[action.status] += 1;
37
+ byType[action.type][action.status] += 1;
38
+ if (bySource[action.source]) bySource[action.source][action.status] += 1;
39
+ if (isFailureStatus(action.status)) ok = false;
40
+ }
41
+
42
+ return {
43
+ ok,
44
+ dryRun: input.dryRun,
45
+ project: input.project,
46
+ force: input.force,
47
+ sources: input.sources,
48
+ destinations: input.destinations,
49
+ summary: { total, byType, bySource },
50
+ actions: input.actions.map(action => ({
51
+ source: action.source,
52
+ type: action.type,
53
+ name: action.name,
54
+ effectiveName: action.effectiveName,
55
+ destination: action.destination,
56
+ operation: action.operation,
57
+ status: action.status,
58
+ reason: action.reason,
59
+ warnings: action.warnings,
60
+ })),
61
+ warnings: input.warnings,
62
+ };
63
+ }
64
+
65
+ function summarizeCounts(counts: StatusCounts): string {
66
+ const parts: string[] = [];
67
+ for (const [status, count] of Object.entries(counts)) {
68
+ if (count > 0) parts.push(`${status}=${count}`);
69
+ }
70
+ return parts.length > 0 ? parts.join(", ") : "nothing";
71
+ }
72
+
73
+ export function renderHuman(report: MigrateReport): string {
74
+ const lines: string[] = [];
75
+ const mode = report.dryRun ? " (dry-run)" : "";
76
+ lines.push(`gjc migrate${mode}: ${report.ok ? "ok" : "completed with failures"}`);
77
+ lines.push(`Sources: ${report.sources.join(", ") || "none"}`);
78
+ lines.push(`Destination: mcp=${report.destinations.mcpConfigPath} skills=${report.destinations.skillsDir}`);
79
+ lines.push("");
80
+ lines.push(`MCP: ${summarizeCounts(report.summary.byType.mcp)}`);
81
+ lines.push(`Skills: ${summarizeCounts(report.summary.byType.skill)}`);
82
+ if (Object.values(report.summary.byType.source).some(n => n > 0)) {
83
+ lines.push(`Source: ${summarizeCounts(report.summary.byType.source)}`);
84
+ }
85
+
86
+ const failures = report.actions.filter(a => isFailureStatus(a.status));
87
+ if (failures.length > 0) {
88
+ lines.push("");
89
+ lines.push("Failures:");
90
+ for (const f of failures) {
91
+ lines.push(` - [${f.source}] ${f.type} ${f.name ?? ""}: ${f.status}${f.reason ? ` (${f.reason})` : ""}`);
92
+ }
93
+ }
94
+
95
+ if (report.warnings.length > 0) {
96
+ lines.push("");
97
+ lines.push("Warnings:");
98
+ for (const w of report.warnings) {
99
+ lines.push(` - [${w.source}] ${w.type} ${w.name ?? ""}: ${w.message}`);
100
+ }
101
+ }
102
+
103
+ return lines.join("\n");
104
+ }