@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,274 @@
1
+ /**
2
+ * Notify CLI command handlers.
3
+ *
4
+ * Handles `gjc notify` setup/status and the hidden daemon entrypoint.
5
+ */
6
+ import { createInterface } from "node:readline/promises";
7
+ import { APP_NAME } from "@gajae-code/utils";
8
+ import chalk from "chalk";
9
+ import { Settings } from "../config/settings";
10
+ import { getNotificationConfig, maskToken } from "../notifications/config";
11
+
12
+ export type NotifyAction = "setup" | "status" | "daemon-internal";
13
+
14
+ export interface NotifyCommandArgs {
15
+ action: NotifyAction;
16
+ smoke?: boolean;
17
+ rawArgs: string[];
18
+ token?: string;
19
+ chatId?: string;
20
+ redact?: boolean;
21
+ }
22
+
23
+ export interface NotifyCommandDeps {
24
+ fetchImpl?: typeof fetch;
25
+ apiBase?: string;
26
+ settings?: Settings;
27
+ setupToken?: string;
28
+ pollTimeoutMs?: number;
29
+ pollIntervalMs?: number;
30
+ setupChatId?: string;
31
+ setupRedact?: boolean;
32
+ }
33
+
34
+ interface TelegramApiResponse<T> {
35
+ ok: boolean;
36
+ result?: T;
37
+ description?: string;
38
+ }
39
+
40
+ interface TelegramUpdate {
41
+ update_id: number;
42
+ message?: {
43
+ chat?: {
44
+ id?: number | string;
45
+ type?: string;
46
+ };
47
+ };
48
+ }
49
+
50
+ const DEFAULT_API_BASE = "https://api.telegram.org";
51
+ const DEFAULT_POLL_TIMEOUT_MS = 60_000;
52
+ const DEFAULT_POLL_INTERVAL_MS = 1_000;
53
+
54
+ export function parseNotifyArgs(args: string[]): NotifyCommandArgs | undefined {
55
+ if (args.length === 0 || args[0] !== "notify") {
56
+ return undefined;
57
+ }
58
+
59
+ const action = args[1];
60
+ if (action === "setup" || action === "status") {
61
+ const rest = args.slice(2);
62
+ const flag = (name: string): string | undefined => {
63
+ const i = rest.indexOf(name);
64
+ return i >= 0 ? rest[i + 1] : undefined;
65
+ };
66
+ return {
67
+ action,
68
+ rawArgs: rest,
69
+ token: flag("--token"),
70
+ chatId: flag("--chat-id"),
71
+ redact: rest.includes("--redact"),
72
+ };
73
+ }
74
+ if (action === "daemon-internal") {
75
+ return {
76
+ action,
77
+ smoke: args.slice(2).includes("--smoke"),
78
+ rawArgs: args.slice(2),
79
+ };
80
+ }
81
+
82
+ return { action: "status", rawArgs: args.slice(1) };
83
+ }
84
+
85
+ export async function runNotifyCommand(cmd: NotifyCommandArgs, deps: NotifyCommandDeps = {}): Promise<void> {
86
+ switch (cmd.action) {
87
+ case "setup":
88
+ await runSetup({
89
+ ...deps,
90
+ setupToken: deps.setupToken ?? cmd.token,
91
+ setupChatId: deps.setupChatId ?? cmd.chatId,
92
+ setupRedact: deps.setupRedact ?? cmd.redact,
93
+ });
94
+ return;
95
+ case "status":
96
+ await runStatus(deps);
97
+ return;
98
+ case "daemon-internal": {
99
+ const m = await import("../notifications/telegram-daemon-cli");
100
+ if (cmd.smoke) {
101
+ await m.runDaemonSmoke();
102
+ } else {
103
+ await m.runDaemonInternal(cmd.rawArgs);
104
+ }
105
+ return;
106
+ }
107
+ }
108
+ }
109
+
110
+ async function getSettings(deps: NotifyCommandDeps): Promise<Settings> {
111
+ return deps.settings ?? (await Settings.init());
112
+ }
113
+
114
+ async function runSetup(deps: NotifyCommandDeps): Promise<void> {
115
+ const settings = await getSettings(deps);
116
+ const fetchImpl = deps.fetchImpl ?? globalThis.fetch;
117
+ const apiBase = deps.apiBase ?? DEFAULT_API_BASE;
118
+ const token = deps.setupToken ?? (await promptForToken());
119
+ if (!token.trim()) {
120
+ throw new Error("Telegram bot token is required.");
121
+ }
122
+
123
+ await callTelegram(fetchImpl, apiBase, token, "getMe", {});
124
+ process.stdout.write(
125
+ "Token validated. Message your bot now from the private Telegram chat to pair notifications.\n",
126
+ );
127
+
128
+ let chatId: string;
129
+ if (deps.setupChatId?.trim()) {
130
+ chatId = deps.setupChatId.trim();
131
+ process.stdout.write(`Using provided chat id ${chatId} (non-interactive).\n`);
132
+ } else {
133
+ const stale = await getUpdates(fetchImpl, apiBase, token, { timeout: 0, allowed_updates: ["message"] });
134
+ const offset = nextOffset(stale);
135
+ chatId = await waitForPrivateChat(fetchImpl, apiBase, token, {
136
+ offset,
137
+ pollTimeoutMs: deps.pollTimeoutMs ?? DEFAULT_POLL_TIMEOUT_MS,
138
+ pollIntervalMs: deps.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS,
139
+ });
140
+ }
141
+
142
+ settings.set("notifications.telegram.botToken", token);
143
+ settings.set("notifications.telegram.chatId", chatId);
144
+ settings.set("notifications.enabled", true);
145
+ if (deps.setupRedact) settings.set("notifications.redact", true);
146
+ await settings.flush();
147
+
148
+ process.stdout.write(`Notifications enabled. botToken=${maskToken(token)} chatId=${chatId}\n`);
149
+ }
150
+
151
+ async function promptForToken(): Promise<string> {
152
+ if (!process.stdin.isTTY) {
153
+ throw new Error("notify setup requires an interactive TTY unless setupToken is injected.");
154
+ }
155
+ const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
156
+ try {
157
+ return (await rl.question("Telegram BotFather token: ")).trim();
158
+ } finally {
159
+ rl.close();
160
+ }
161
+ }
162
+
163
+ async function runStatus(deps: NotifyCommandDeps): Promise<void> {
164
+ const settings = await getSettings(deps);
165
+ const cfg = getNotificationConfig(settings);
166
+ process.stdout.write(
167
+ `${chalk.bold("Notifications")}\n` +
168
+ ` enabled: ${cfg.enabled}\n` +
169
+ ` botToken: ${maskToken(cfg.botToken)}\n` +
170
+ ` chatId: ${cfg.chatId ?? "(unset)"}\n` +
171
+ ` redact: ${cfg.redact}\n`,
172
+ );
173
+ }
174
+
175
+ async function waitForPrivateChat(
176
+ fetchImpl: typeof fetch,
177
+ apiBase: string,
178
+ token: string,
179
+ opts: { offset: number | undefined; pollTimeoutMs: number; pollIntervalMs: number },
180
+ ): Promise<string> {
181
+ const deadline = Date.now() + opts.pollTimeoutMs;
182
+ let offset = opts.offset;
183
+ let sawRejectedChatType: string | undefined;
184
+
185
+ while (Date.now() <= deadline) {
186
+ const updates = await getUpdates(fetchImpl, apiBase, token, { offset, timeout: 0, allowed_updates: ["message"] });
187
+ offset = nextOffset(updates, offset);
188
+ for (const update of updates) {
189
+ const chat = update.message?.chat;
190
+ if (!chat) continue;
191
+ if (chat.type === "private" && chat.id !== undefined) {
192
+ return String(chat.id);
193
+ }
194
+ if (chat.type === "group" || chat.type === "supergroup" || chat.type === "channel") {
195
+ sawRejectedChatType = chat.type;
196
+ process.stderr.write(
197
+ `Rejected ${chat.type} chat. Pairing requires a private Telegram chat with the bot.\n`,
198
+ );
199
+ }
200
+ }
201
+ if (opts.pollIntervalMs > 0) {
202
+ await new Promise(resolve =>
203
+ setTimeout(resolve, Math.min(opts.pollIntervalMs, Math.max(0, deadline - Date.now()))),
204
+ );
205
+ }
206
+ }
207
+
208
+ if (sawRejectedChatType) {
209
+ throw new Error(`Pairing rejected ${sawRejectedChatType} chat; message the bot from a private chat.`);
210
+ }
211
+ throw new Error("Timed out waiting for a private Telegram message to pair notifications.");
212
+ }
213
+
214
+ function nextOffset(updates: TelegramUpdate[], fallback?: number): number | undefined {
215
+ let max = fallback === undefined ? undefined : fallback - 1;
216
+ for (const update of updates) {
217
+ if (typeof update.update_id === "number" && (max === undefined || update.update_id > max)) {
218
+ max = update.update_id;
219
+ }
220
+ }
221
+ return max === undefined ? fallback : max + 1;
222
+ }
223
+
224
+ async function getUpdates(
225
+ fetchImpl: typeof fetch,
226
+ apiBase: string,
227
+ token: string,
228
+ params: Record<string, unknown>,
229
+ ): Promise<TelegramUpdate[]> {
230
+ return await callTelegram<TelegramUpdate[]>(fetchImpl, apiBase, token, "getUpdates", params);
231
+ }
232
+
233
+ async function callTelegram<T>(
234
+ fetchImpl: typeof fetch,
235
+ apiBase: string,
236
+ token: string,
237
+ method: string,
238
+ body: Record<string, unknown>,
239
+ ): Promise<T> {
240
+ const response = await fetchImpl(`${apiBase.replace(/\/$/, "")}/bot${token}/${method}`, {
241
+ method: "POST",
242
+ headers: { "content-type": "application/json" },
243
+ body: JSON.stringify(body),
244
+ });
245
+ let payload: TelegramApiResponse<T>;
246
+ try {
247
+ payload = (await response.json()) as TelegramApiResponse<T>;
248
+ } catch {
249
+ throw new Error(`Telegram ${method} returned invalid JSON.`);
250
+ }
251
+ if (!response.ok || !payload.ok) {
252
+ throw new Error(`Telegram ${method} failed: ${payload.description ?? response.statusText}`);
253
+ }
254
+ return payload.result as T;
255
+ }
256
+
257
+ export function printNotifyHelp(): void {
258
+ process.stdout.write(`${chalk.bold(`${APP_NAME} notify`)} - Configure Telegram notifications
259
+
260
+ ${chalk.bold("Usage:")}
261
+ ${APP_NAME} notify setup
262
+ ${APP_NAME} notify setup --token <botToken> --chat-id <chatId> [--redact]
263
+ ${APP_NAME} notify status
264
+
265
+ ${chalk.bold("Subcommands:")}
266
+ setup Pair a Telegram bot token with a private chat
267
+ status Show notification configuration without secrets
268
+
269
+ ${chalk.bold("Examples:")}
270
+ ${APP_NAME} notify setup
271
+ ${APP_NAME} notify setup --token <botToken> --chat-id <chatId> [--redact]
272
+ ${APP_NAME} notify status
273
+ `);
274
+ }
@@ -6,7 +6,7 @@
6
6
 
7
7
  import * as path from "node:path";
8
8
  import { createInterface } from "node:readline/promises";
9
- import { SqliteAuthCredentialStore } from "@gajae-code/ai";
9
+ import { AuthStorage, SqliteAuthCredentialStore } from "@gajae-code/ai";
10
10
  import { $which, APP_NAME, getAgentDbPath, getPythonEnvDir } from "@gajae-code/utils";
11
11
  import { $ } from "bun";
12
12
  import chalk from "chalk";
@@ -17,13 +17,15 @@ import {
17
17
  readGjcManagedCodexHooksStatus,
18
18
  } from "../hooks/codex-native-hooks-config";
19
19
  import { theme } from "../modes/theme/theme";
20
- import { discoverExternalCredentials, formatDiscoverySummary, importCredentials } from "../setup/credential-import";
20
+ import { formatCredentialAutoImportResult, runExternalCredentialAutoImport } from "../setup/credential-auto-import";
21
+ import { filterAutoImportOAuthCredentials, formatDiscoverySummary } from "../setup/credential-import";
21
22
  import {
22
23
  formatHermesSetupResult,
23
24
  type HermesSetupFlags,
24
25
  hermesSetupExitCode,
25
26
  runHermesSetup,
26
27
  } from "../setup/hermes-setup";
28
+ import { buildHostPluginSetup, formatHostPluginSetup, type HostPluginKind } from "../setup/host-plugin-setup";
27
29
  import {
28
30
  addApiCompatibleProvider,
29
31
  formatProviderPresetList,
@@ -31,7 +33,16 @@ import {
31
33
  parseProviderCompatibility,
32
34
  } from "../setup/provider-onboarding";
33
35
 
34
- export type SetupComponent = "credentials" | "defaults" | "hermes" | "hooks" | "provider" | "python" | "stt";
36
+ export type SetupComponent =
37
+ | "claude"
38
+ | "codex"
39
+ | "credentials"
40
+ | "defaults"
41
+ | "hermes"
42
+ | "hooks"
43
+ | "provider"
44
+ | "python"
45
+ | "stt";
35
46
 
36
47
  export interface SetupCommandArgs {
37
48
  component: SetupComponent;
@@ -63,10 +74,21 @@ export interface SetupCommandArgs {
63
74
  profileDir?: string;
64
75
  yes?: boolean;
65
76
  dryRun?: boolean;
77
+ keychain?: boolean;
66
78
  };
67
79
  }
68
80
 
69
- const VALID_COMPONENTS: SetupComponent[] = ["credentials", "defaults", "hermes", "hooks", "provider", "python", "stt"];
81
+ const VALID_COMPONENTS: SetupComponent[] = [
82
+ "claude",
83
+ "codex",
84
+ "credentials",
85
+ "defaults",
86
+ "hermes",
87
+ "hooks",
88
+ "provider",
89
+ "python",
90
+ "stt",
91
+ ];
70
92
 
71
93
  function hasProviderSetupFlags(flags: SetupCommandArgs["flags"]): boolean {
72
94
  return (
@@ -123,6 +145,8 @@ export function parseSetupArgs(args: string[]): SetupCommandArgs | undefined {
123
145
  flags.yes = true;
124
146
  } else if (arg === "--dry-run") {
125
147
  flags.dryRun = true;
148
+ } else if (arg === "--keychain") {
149
+ flags.keychain = true;
126
150
  } else if (arg === "--root") {
127
151
  flags.root = [...(flags.root ?? []), args[++i] ?? ""];
128
152
  } else if (arg === "--repo") {
@@ -235,6 +259,12 @@ async function checkPythonSetup(): Promise<PythonCheckResult> {
235
259
  export async function runSetupCommand(cmd: SetupCommandArgs): Promise<void> {
236
260
  rejectProviderFlagsOutsideProvider(cmd.component, cmd.flags);
237
261
  switch (cmd.component) {
262
+ case "claude":
263
+ handleHostPluginSetup("claude", cmd.flags);
264
+ break;
265
+ case "codex":
266
+ handleHostPluginSetup("codex", cmd.flags);
267
+ break;
238
268
  case "defaults":
239
269
  await handleDefaultsSetup(cmd.flags);
240
270
  break;
@@ -279,6 +309,22 @@ async function handleHermesSetup(flags: HermesSetupFlags): Promise<void> {
279
309
  process.exit(hermesSetupExitCode(error));
280
310
  }
281
311
  }
312
+
313
+ function handleHostPluginSetup(host: HostPluginKind, flags: SetupCommandArgs["flags"]): void {
314
+ const result = buildHostPluginSetup(host, {
315
+ json: flags.json,
316
+ check: flags.check,
317
+ root: flags.root,
318
+ repo: flags.repo,
319
+ });
320
+ if (flags.json) {
321
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
322
+ return;
323
+ }
324
+ const label = host === "claude" ? "Claude Code" : "Codex";
325
+ process.stdout.write(`${chalk.green(`${theme.status.success} ${label} plugin setup ready`)}\n`);
326
+ process.stdout.write(`${chalk.dim(formatHostPluginSetup(result))}\n`);
327
+ }
282
328
  async function handleProviderSetup(flags: {
283
329
  json?: boolean;
284
330
  force?: boolean;
@@ -516,99 +562,141 @@ async function confirmImport(count: number): Promise<boolean> {
516
562
  * gjc credential store after a redacted preview + confirmation. Falls back to
517
563
  * manual-setup guidance when nothing importable is found.
518
564
  */
519
- async function handleCredentialsSetup(flags: { json?: boolean; yes?: boolean; dryRun?: boolean }): Promise<void> {
520
- const result = await discoverExternalCredentials();
521
- const redactedPlan = {
522
- importable: result.importable.map(c => ({
523
- provider: c.provider,
524
- kind: c.kind,
525
- source: c.source,
526
- identity: c.identity,
527
- expiresAt: c.expiresAt,
528
- redactedToken: c.redactedToken,
529
- })),
530
- skipped: result.skipped,
531
- environment: result.environment,
532
- };
565
+ export interface CredentialsSetupDependencies {
566
+ openStore?: typeof SqliteAuthCredentialStore.open;
567
+ createAuthStorage?: (store: Awaited<ReturnType<typeof SqliteAuthCredentialStore.open>>) => AuthStorage;
568
+ discover?: Parameters<typeof runExternalCredentialAutoImport>[0]["discover"];
569
+ }
533
570
 
534
- if (result.importable.length === 0) {
535
- if (flags.json) {
536
- process.stdout.write(`${JSON.stringify({ ...redactedPlan, imported: [] })}\n`);
571
+ export async function handleCredentialsSetup(
572
+ flags: {
573
+ json?: boolean;
574
+ yes?: boolean;
575
+ dryRun?: boolean;
576
+ keychain?: boolean;
577
+ },
578
+ deps: CredentialsSetupDependencies = {},
579
+ ): Promise<void> {
580
+ const discoveryOptions = flags.keychain ? undefined : { readClaudeKeychain: async () => null };
581
+ const store = await (deps.openStore ?? SqliteAuthCredentialStore.open)(getAgentDbPath());
582
+ const authStorage = deps.createAuthStorage?.(store) ?? new AuthStorage(store);
583
+ await authStorage.reload();
584
+ try {
585
+ const preview = await runExternalCredentialAutoImport({
586
+ authStorage: {
587
+ importCredentialIfAbsent: async () => ({
588
+ inserted: false,
589
+ reason: "skipped-existing",
590
+ provider: "",
591
+ entries: [],
592
+ }),
593
+ },
594
+ discover: deps.discover,
595
+ discoveryOptions,
596
+ trigger: "setup-cli",
597
+ });
598
+ const result = preview.discovery ?? { importable: [], skipped: [], environment: [] };
599
+ const candidates = filterAutoImportOAuthCredentials(result.importable);
600
+ const filteredResult = { ...result, importable: candidates };
601
+ const redactedPlan = {
602
+ importable: candidates.map(c => ({
603
+ provider: c.provider,
604
+ kind: c.kind,
605
+ source: c.source,
606
+ identity: c.identity,
607
+ expiresAt: c.expiresAt,
608
+ redactedToken: c.redactedToken,
609
+ })),
610
+ skipped: result.skipped,
611
+ environment: result.environment,
612
+ keychainChecked: flags.keychain === true,
613
+ };
614
+
615
+ if (!flags.keychain && !flags.json) {
616
+ process.stdout.write(chalk.dim("Claude Keychain not checked (pass --keychain to include it)\n"));
617
+ }
618
+
619
+ if (candidates.length === 0) {
620
+ if (flags.json) {
621
+ process.stdout.write(`${JSON.stringify({ ...redactedPlan, imported: [] })}\n`);
622
+ return;
623
+ }
624
+ for (const line of formatDiscoverySummary(filteredResult)) process.stdout.write(` ${line}\n`);
625
+ process.stdout.write(
626
+ chalk.yellow(
627
+ `\nNo importable Claude/Codex credentials found. Continue with manual setup:\n` +
628
+ ` ${APP_NAME} setup provider (add an API-compatible provider)\n` +
629
+ ` ${APP_NAME} (then /login) (interactive OAuth/subscription login)\n`,
630
+ ),
631
+ );
537
632
  return;
538
633
  }
539
- for (const line of formatDiscoverySummary(result)) process.stdout.write(` ${line}\n`);
540
- process.stdout.write(
541
- chalk.yellow(
542
- `\nNo importable Claude/Codex credentials found. Continue with manual setup:\n` +
543
- ` ${APP_NAME} setup provider (add an API-compatible provider)\n` +
544
- ` ${APP_NAME} (then /login) (interactive OAuth/subscription login)\n`,
545
- ),
546
- );
547
- return;
548
- }
549
634
 
550
- if (!flags.json) {
551
- process.stdout.write(chalk.bold("Discovered credentials (redacted):\n"));
552
- for (const line of formatDiscoverySummary(result)) process.stdout.write(` ${line}\n`);
553
- }
635
+ if (!flags.json) {
636
+ process.stdout.write(chalk.bold("Discovered credentials (redacted):\n"));
637
+ for (const line of formatDiscoverySummary(filteredResult)) process.stdout.write(` ${line}\n`);
638
+ }
554
639
 
555
- if (flags.dryRun) {
556
- if (flags.json) process.stdout.write(`${JSON.stringify({ ...redactedPlan, dryRun: true, imported: [] })}\n`);
557
- else process.stdout.write(chalk.dim(`\nDry run — no credentials imported.\n`));
558
- return;
559
- }
640
+ if (flags.dryRun) {
641
+ if (flags.json) process.stdout.write(`${JSON.stringify({ ...redactedPlan, dryRun: true, imported: [] })}\n`);
642
+ else process.stdout.write(chalk.dim(`\nDry run — no credentials imported.\n`));
643
+ return;
644
+ }
645
+
646
+ const confirmed = flags.yes || (await confirmImport(candidates.length));
647
+ if (!confirmed) {
648
+ if (flags.json) {
649
+ process.stdout.write(`${JSON.stringify({ ...redactedPlan, imported: [] })}\n`);
650
+ return;
651
+ }
652
+ process.stdout.write(chalk.dim(`\nImport cancelled. Re-run with --yes to import non-interactively.\n`));
653
+ return;
654
+ }
655
+
656
+ const summary = await runExternalCredentialAutoImport({
657
+ authStorage,
658
+ discover: deps.discover,
659
+ discoveryOptions,
660
+ trigger: "setup-cli",
661
+ });
560
662
 
561
- const confirmed = flags.yes || (await confirmImport(result.importable.length));
562
- if (!confirmed) {
563
663
  if (flags.json) {
564
- process.stdout.write(`${JSON.stringify({ ...redactedPlan, imported: [] })}\n`);
664
+ process.stdout.write(
665
+ `${JSON.stringify({
666
+ ...redactedPlan,
667
+ imported: summary.imported.map(c => ({ provider: c.provider, kind: c.kind, source: c.source })),
668
+ skippedImport: summary.skipped.map(s => ({
669
+ provider: s.credential.provider,
670
+ source: s.credential.source,
671
+ reason: s.reason,
672
+ })),
673
+ failed: summary.failures.map(f => ({
674
+ provider: f.credential?.provider,
675
+ source: f.credential?.source ?? f.source,
676
+ error: f.failureClass,
677
+ })),
678
+ })}\n`,
679
+ );
680
+ if (summary.failures.length > 0) process.exitCode = 1;
565
681
  return;
566
682
  }
567
- process.stdout.write(chalk.dim(`\nImport cancelled. Re-run with --yes to import non-interactively.\n`));
568
- return;
569
- }
570
683
 
571
- const store = await SqliteAuthCredentialStore.open(getAgentDbPath());
572
- let summary: Awaited<ReturnType<typeof importCredentials>>;
573
- try {
574
- summary = await importCredentials(result.importable, (provider, credential) =>
575
- store.upsertAuthCredentialForProvider(provider, credential),
576
- );
684
+ for (const credential of summary.imported) {
685
+ process.stdout.write(
686
+ `${chalk.green(`${theme.status.success} imported`)} ${formatCredentialSummaryLine(credential)}\n`,
687
+ );
688
+ }
689
+ for (const line of formatCredentialAutoImportResult({ ...summary, imported: [], skipped: [] })) {
690
+ process.stdout.write(`${chalk.dim(line)}\n`);
691
+ }
692
+ if (summary.failures.length > 0) {
693
+ process.exitCode = 1;
694
+ return;
695
+ }
696
+ process.stdout.write(chalk.dim(`\nCredentials saved to ${getAgentDbPath()}\n`));
577
697
  } finally {
578
698
  store.close();
579
699
  }
580
-
581
- if (flags.json) {
582
- process.stdout.write(
583
- `${JSON.stringify({
584
- ...redactedPlan,
585
- imported: summary.imported.map(c => ({ provider: c.provider, kind: c.kind, source: c.source })),
586
- failed: summary.failed.map(f => ({
587
- provider: f.credential.provider,
588
- source: f.credential.source,
589
- error: f.error,
590
- })),
591
- })}\n`,
592
- );
593
- if (summary.failed.length > 0) process.exitCode = 1;
594
- return;
595
- }
596
-
597
- for (const credential of summary.imported) {
598
- process.stdout.write(
599
- `${chalk.green(`${theme.status.success} imported`)} ${formatCredentialSummaryLine(credential)}\n`,
600
- );
601
- }
602
- for (const failure of summary.failed) {
603
- process.stdout.write(
604
- `${chalk.red(`${theme.status.error} failed`)} ${failure.credential.provider} (${failure.credential.source}): ${failure.error}\n`,
605
- );
606
- }
607
- if (summary.failed.length > 0) {
608
- process.exitCode = 1;
609
- return;
610
- }
611
- process.stdout.write(chalk.dim(`\nCredentials saved to ${getAgentDbPath()}\n`));
612
700
  }
613
701
 
614
702
  function formatCredentialSummaryLine(credential: { provider: string; kind: string; source: string }): string {
@@ -669,6 +757,7 @@ ${chalk.bold("Options:")}
669
757
  --profile-dir Hermes profile directory for full setup install
670
758
  --dry-run Preview discovered credentials without importing (credentials)
671
759
  -y, --yes Import discovered credentials without an interactive prompt (credentials)
760
+ --keychain Include Claude macOS Keychain when discovering credentials
672
761
 
673
762
  ${chalk.bold("Examples:")}
674
763
  ${APP_NAME} setup Install bundled GJC default workflow skills
package/src/cli.ts CHANGED
@@ -35,6 +35,8 @@ const commands: CommandEntry[] = [
35
35
  { name: "gc", load: () => import("./commands/gc").then(m => m.default) },
36
36
  { name: "ralplan", load: () => import("./commands/ralplan").then(m => m.default) },
37
37
  { name: "config", load: () => import("./commands/config").then(m => m.default) },
38
+ { name: "notify", load: () => import("./commands/notify").then(m => m.default) },
39
+ { name: "daemon", load: () => import("./commands/daemon").then(m => m.default) },
38
40
  { name: "web-search", aliases: ["q"], load: () => import("./commands/web-search").then(m => m.default) },
39
41
  { name: "mcp-serve", load: () => import("./commands/mcp-serve").then(m => m.default) },
40
42
  {
@@ -43,6 +45,7 @@ const commands: CommandEntry[] = [
43
45
  load: () => import("./commands/contribution-prep").then(m => m.default),
44
46
  },
45
47
  { name: "deep-interview", load: () => import("./commands/deep-interview").then(m => m.default) },
48
+ { name: "migrate", load: () => import("./commands/migrate").then(m => m.default) },
46
49
  { name: "rlm", load: () => import("./commands/rlm").then(m => m.default) },
47
50
  { name: "update", load: () => import("./commands/update").then(m => m.default) },
48
51
  { name: "launch", load: () => import("./commands/launch").then(m => m.default) },
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Manage GJC background daemons (status/list/stop/reload).
3
+ */
4
+ import { Args, Command, Flags } from "@gajae-code/utils/cli";
5
+ import { type DaemonCliAction, type DaemonCommandArgs, runDaemonCommand } from "../cli/daemon-cli";
6
+ import type { DaemonKind } from "../daemon/control-types";
7
+ import { initTheme } from "../modes/theme/theme";
8
+
9
+ const ACTIONS: DaemonCliAction[] = ["list", "status", "stop", "reload"];
10
+
11
+ export default class Daemon extends Command {
12
+ static description = "Manage GJC background daemons (status, list, stop, reload)";
13
+
14
+ static args = {
15
+ action: Args.string({ description: "Daemon action", required: false, options: ACTIONS }),
16
+ kind: Args.string({ description: "Daemon kind(s) to target", required: false, multiple: true }),
17
+ };
18
+
19
+ static flags = {
20
+ all: Flags.boolean({ description: "Target all registered daemon kinds" }),
21
+ json: Flags.boolean({ description: "Emit JSON output" }),
22
+ force: Flags.boolean({ description: "Allow hard-kill escalation when graceful stop times out" }),
23
+ "graceful-timeout-ms": Flags.integer({ description: "Cooperative stop timeout before escalation" }),
24
+ "kill-timeout-ms": Flags.integer({ description: "Wait for old pid death after SIGKILL" }),
25
+ "spawn-if-stopped": Flags.boolean({ description: "On reload, spawn even when no daemon is running" }),
26
+ };
27
+
28
+ async run(): Promise<void> {
29
+ const { args, flags } = await this.parse(Daemon);
30
+ const action = (args.action ?? "status") as DaemonCliAction;
31
+ const kinds = (Array.isArray(args.kind) ? args.kind : args.kind ? [args.kind] : []) as DaemonKind[];
32
+ const flagRec = flags as Record<string, unknown>;
33
+ const cmd: DaemonCommandArgs = {
34
+ action,
35
+ kinds,
36
+ all: Boolean(flags.all),
37
+ json: Boolean(flags.json),
38
+ force: Boolean(flags.force),
39
+ gracefulTimeoutMs: flagRec["graceful-timeout-ms"] as number | undefined,
40
+ killTimeoutMs: flagRec["kill-timeout-ms"] as number | undefined,
41
+ spawnIfStopped: flagRec["spawn-if-stopped"] as boolean | undefined,
42
+ };
43
+
44
+ await initTheme();
45
+ await runDaemonCommand(cmd);
46
+ }
47
+ }
@@ -11,11 +11,11 @@ export default class DeepInterview extends Command {
11
11
  threshold: Flags.string({ description: "Override ambiguity threshold for kickoff" }),
12
12
  "threshold-source": Flags.string({ description: "Describe the threshold override source" }),
13
13
  "session-id": Flags.string({
14
- description: "Route state/spec handoff through a session-scoped .gjc state directory",
14
+ description: "Route state/spec handoff through a session-scoped .gjc/_session-{sessionid} directory",
15
15
  }),
16
16
  write: Flags.boolean({ description: "Persist a final deep-interview spec through the sanctioned GJC CLI/API" }),
17
17
  stage: Flags.string({ description: 'Spec stage for --write (currently "final")' }),
18
- slug: Flags.string({ description: "Safe slug for .gjc/specs/deep-interview-<slug>.md" }),
18
+ slug: Flags.string({ description: "Safe slug for .gjc/_session-{sessionid}/specs/deep-interview-<slug>.md" }),
19
19
  spec: Flags.string({ description: "Final spec markdown or a path to the final spec markdown" }),
20
20
  handoff: Flags.string({ description: 'After --write, hand off to a workflow target (currently "ralplan")' }),
21
21
  deliberate: Flags.boolean({