@gajae-code/coding-agent 0.6.5 → 0.7.1

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 (135) hide show
  1. package/CHANGELOG.md +38 -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/notify-cli.d.ts +23 -0
  5. package/dist/types/cli/setup-cli.d.ts +20 -1
  6. package/dist/types/commands/daemon.d.ts +41 -0
  7. package/dist/types/commands/notify.d.ts +41 -0
  8. package/dist/types/config/model-profile-activation.d.ts +12 -0
  9. package/dist/types/config/model-profiles.d.ts +2 -1
  10. package/dist/types/config/model-registry.d.ts +3 -3
  11. package/dist/types/config/models-config-schema.d.ts +5 -0
  12. package/dist/types/config/settings-schema.d.ts +38 -0
  13. package/dist/types/coordinator/contract.d.ts +1 -1
  14. package/dist/types/daemon/builtin.d.ts +20 -0
  15. package/dist/types/daemon/control-types.d.ts +57 -0
  16. package/dist/types/daemon/runtime.d.ts +25 -0
  17. package/dist/types/extensibility/extensions/types.d.ts +8 -0
  18. package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
  19. package/dist/types/gjc-runtime/state-writer.d.ts +2 -0
  20. package/dist/types/gjc-runtime/tmux-common.d.ts +3 -0
  21. package/dist/types/gjc-runtime/tmux-sessions.d.ts +2 -0
  22. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +15 -0
  23. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +14 -0
  24. package/dist/types/modes/components/oauth-selector.d.ts +2 -0
  25. package/dist/types/modes/controllers/selector-controller.d.ts +2 -2
  26. package/dist/types/modes/interactive-mode.d.ts +1 -1
  27. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
  28. package/dist/types/modes/types.d.ts +7 -1
  29. package/dist/types/notifications/config-commands.d.ts +26 -0
  30. package/dist/types/notifications/config.d.ts +61 -0
  31. package/dist/types/notifications/helpers.d.ts +55 -0
  32. package/dist/types/notifications/html-format.d.ts +62 -0
  33. package/dist/types/notifications/index.d.ts +28 -0
  34. package/dist/types/notifications/rate-limit-pool.d.ts +93 -0
  35. package/dist/types/notifications/telegram-cli.d.ts +19 -0
  36. package/dist/types/notifications/telegram-daemon-cli.d.ts +11 -0
  37. package/dist/types/notifications/telegram-daemon-control.d.ts +56 -0
  38. package/dist/types/notifications/telegram-daemon.d.ts +276 -0
  39. package/dist/types/notifications/telegram-reference.d.ts +111 -0
  40. package/dist/types/notifications/threaded-inbound.d.ts +58 -0
  41. package/dist/types/notifications/threaded-render.d.ts +66 -0
  42. package/dist/types/notifications/topic-registry.d.ts +67 -0
  43. package/dist/types/rlm/index.d.ts +12 -0
  44. package/dist/types/session/agent-session.d.ts +39 -2
  45. package/dist/types/session/auth-storage.d.ts +1 -1
  46. package/dist/types/setup/credential-auto-import.d.ts +63 -0
  47. package/dist/types/setup/credential-import.d.ts +3 -0
  48. package/dist/types/setup/host-plugin-setup.d.ts +39 -0
  49. package/dist/types/tools/ask-answer-registry.d.ts +13 -0
  50. package/dist/types/tools/index.d.ts +18 -0
  51. package/dist/types/tools/subagent.d.ts +3 -0
  52. package/package.json +7 -7
  53. package/scripts/build-binary.ts +3 -0
  54. package/src/async/job-manager.ts +5 -1
  55. package/src/cli/daemon-cli.ts +122 -0
  56. package/src/cli/notify-cli.ts +274 -0
  57. package/src/cli/setup-cli.ts +173 -84
  58. package/src/cli.ts +3 -3
  59. package/src/commands/daemon.ts +47 -0
  60. package/src/commands/notify.ts +61 -0
  61. package/src/commands/setup.ts +11 -1
  62. package/src/config/model-profile-activation.ts +74 -5
  63. package/src/config/model-profiles.ts +7 -4
  64. package/src/config/model-registry.ts +6 -3
  65. package/src/config/models-config-schema.ts +1 -1
  66. package/src/config/settings-schema.ts +29 -0
  67. package/src/coordinator/contract.ts +3 -0
  68. package/src/coordinator-mcp/server.ts +270 -1
  69. package/src/daemon/builtin.ts +46 -0
  70. package/src/daemon/control-types.ts +65 -0
  71. package/src/daemon/runtime.ts +51 -0
  72. package/src/defaults/gjc/skills/ultragoal/SKILL.md +16 -0
  73. package/src/edit/modes/replace.ts +1 -1
  74. package/src/extensibility/extensions/runner.ts +4 -0
  75. package/src/extensibility/extensions/types.ts +8 -0
  76. package/src/gjc-runtime/deep-interview-recorder.ts +12 -4
  77. package/src/gjc-runtime/launch-tmux.ts +10 -2
  78. package/src/gjc-runtime/state-runtime.ts +18 -4
  79. package/src/gjc-runtime/state-writer.ts +8 -8
  80. package/src/gjc-runtime/tmux-common.ts +8 -0
  81. package/src/gjc-runtime/tmux-sessions.ts +8 -1
  82. package/src/gjc-runtime/ultragoal-guard.ts +57 -2
  83. package/src/gjc-runtime/ultragoal-runtime.ts +105 -19
  84. package/src/gjc-runtime/workflow-manifest.generated.json +27 -2
  85. package/src/gjc-runtime/workflow-manifest.ts +11 -1
  86. package/src/goals/tools/goal-tool.ts +11 -2
  87. package/src/hashline/hash.ts +1 -1
  88. package/src/internal-urls/docs-index.generated.ts +9 -7
  89. package/src/main.ts +30 -0
  90. package/src/modes/acp/acp-event-mapper.ts +1 -0
  91. package/src/modes/components/hook-editor.ts +7 -2
  92. package/src/modes/components/oauth-selector.ts +19 -0
  93. package/src/modes/controllers/event-controller.ts +20 -0
  94. package/src/modes/controllers/selector-controller.ts +80 -17
  95. package/src/modes/interactive-mode.ts +6 -2
  96. package/src/modes/runtime-init.ts +1 -0
  97. package/src/modes/shared/agent-wire/event-contract.ts +1 -0
  98. package/src/modes/shared/agent-wire/event-envelope.ts +1 -0
  99. package/src/modes/shared/agent-wire/event-observation.ts +16 -0
  100. package/src/modes/shared/agent-wire/unattended-session.ts +22 -0
  101. package/src/modes/types.ts +7 -1
  102. package/src/modes/utils/ui-helpers.ts +23 -0
  103. package/src/notifications/config-commands.ts +50 -0
  104. package/src/notifications/config.ts +107 -0
  105. package/src/notifications/helpers.ts +135 -0
  106. package/src/notifications/html-format.ts +389 -0
  107. package/src/notifications/index.ts +700 -0
  108. package/src/notifications/rate-limit-pool.ts +179 -0
  109. package/src/notifications/telegram-cli.ts +194 -0
  110. package/src/notifications/telegram-daemon-cli.ts +74 -0
  111. package/src/notifications/telegram-daemon-control.ts +370 -0
  112. package/src/notifications/telegram-daemon.ts +1370 -0
  113. package/src/notifications/telegram-reference.ts +335 -0
  114. package/src/notifications/threaded-inbound.ts +80 -0
  115. package/src/notifications/threaded-render.ts +155 -0
  116. package/src/notifications/topic-registry.ts +133 -0
  117. package/src/rlm/index.ts +19 -0
  118. package/src/sdk.ts +16 -0
  119. package/src/session/agent-session.ts +113 -3
  120. package/src/session/auth-storage.ts +3 -0
  121. package/src/session/session-dump-format.ts +43 -2
  122. package/src/session/session-manager.ts +39 -5
  123. package/src/setup/credential-auto-import.ts +258 -0
  124. package/src/setup/credential-import.ts +17 -0
  125. package/src/setup/hermes/templates/operator-instructions.v1.md +10 -0
  126. package/src/setup/host-plugin-setup.ts +142 -0
  127. package/src/slash-commands/builtin-registry.ts +4 -1
  128. package/src/task/executor.ts +5 -1
  129. package/src/tools/ask-answer-registry.ts +25 -0
  130. package/src/tools/ask.ts +77 -6
  131. package/src/tools/image-gen.ts +5 -8
  132. package/src/tools/index.ts +19 -0
  133. package/src/tools/inspect-image.ts +16 -11
  134. package/src/tools/subagent-render.ts +7 -0
  135. package/src/tools/subagent.ts +38 -7
@@ -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
  {
@@ -174,9 +176,7 @@ async function runSmokeTest(): Promise<void> {
174
176
  // the COMPILED single binary (dev runs only load the on-disk .node). Loading the
175
177
  // natives module triggers loadNative()/embedded extraction; calling each new
176
178
  // export confirms the symbols are present in the shipped binary.
177
- const { h06FormatHashLines, h02ScoreSequenceFuzzy, h01FindBestFuzzyMatch } = await import(
178
- "../../natives/native/index.js"
179
- );
179
+ const { h06FormatHashLines, h02ScoreSequenceFuzzy, h01FindBestFuzzyMatch } = await import("@gajae-code/natives");
180
180
  const hashed = h06FormatHashLines("a\nb", 1);
181
181
  if (hashed.split("\n").length !== 2) {
182
182
  throw new Error(`smoke-test: h06FormatHashLines returned unexpected output: ${JSON.stringify(hashed)}`);
@@ -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
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Configure Telegram notifications.
3
+ */
4
+ import { Args, Command, Flags } from "@gajae-code/utils/cli";
5
+ import { type NotifyAction, type NotifyCommandArgs, runNotifyCommand } from "../cli/notify-cli";
6
+ import { initTheme } from "../modes/theme/theme";
7
+
8
+ const ACTIONS: NotifyAction[] = ["setup", "status", "daemon-internal"];
9
+
10
+ export default class Notify extends Command {
11
+ static description = "Configure Telegram notifications";
12
+
13
+ static args = {
14
+ action: Args.string({
15
+ description: "Notify action",
16
+ required: false,
17
+ options: ACTIONS,
18
+ }),
19
+ extra: Args.string({
20
+ description: "Additional internal args",
21
+ required: false,
22
+ multiple: true,
23
+ }),
24
+ };
25
+
26
+ static flags = {
27
+ smoke: Flags.boolean({ description: "Run hidden daemon smoke" }),
28
+ token: Flags.string({ description: "Telegram bot token (non-interactive setup)" }),
29
+ "chat-id": Flags.string({ description: "Telegram chat id to pair (non-interactive setup)" }),
30
+ redact: Flags.boolean({ description: "Enable redaction of remote notification content" }),
31
+ "owner-id": Flags.string({ description: "Internal: daemon owner id" }),
32
+ "agent-dir": Flags.string({ description: "Internal: agent dir for the daemon" }),
33
+ };
34
+
35
+ async run(): Promise<void> {
36
+ const { args, flags } = await this.parse(Notify);
37
+ const action = (args.action ?? "status") as NotifyAction;
38
+ const extra = Array.isArray(args.extra) ? args.extra : args.extra ? [args.extra] : [];
39
+ const flagRec = flags as Record<string, unknown>;
40
+ const ownerId = flagRec["owner-id"] as string | undefined;
41
+ const agentDir = flagRec["agent-dir"] as string | undefined;
42
+ const rawArgs = [
43
+ ...(flags.smoke ? ["--smoke"] : []),
44
+ ...(ownerId ? ["--owner-id", ownerId] : []),
45
+ ...(agentDir ? ["--agent-dir", agentDir] : []),
46
+ ...extra,
47
+ ];
48
+
49
+ const cmd: NotifyCommandArgs = {
50
+ action,
51
+ smoke: flags.smoke,
52
+ rawArgs,
53
+ token: flags.token as string | undefined,
54
+ chatId: (flags as Record<string, unknown>)["chat-id"] as string | undefined,
55
+ redact: Boolean(flags.redact),
56
+ };
57
+
58
+ await initTheme();
59
+ await runNotifyCommand(cmd);
60
+ }
61
+ }
@@ -5,7 +5,17 @@ import { Args, Command, Flags } from "@gajae-code/utils/cli";
5
5
  import { runSetupCommand, type SetupCommandArgs, type SetupComponent } from "../cli/setup-cli";
6
6
  import { initTheme } from "../modes/theme/theme";
7
7
 
8
- const COMPONENTS: SetupComponent[] = ["credentials", "defaults", "hermes", "hooks", "provider", "python", "stt"];
8
+ const COMPONENTS: SetupComponent[] = [
9
+ "claude",
10
+ "codex",
11
+ "credentials",
12
+ "defaults",
13
+ "hermes",
14
+ "hooks",
15
+ "provider",
16
+ "python",
17
+ "stt",
18
+ ];
9
19
 
10
20
  export default class Setup extends Command {
11
21
  static description = "Install GJC defaults or optional feature dependencies";
@@ -17,6 +17,8 @@ type ModelProfileActivationSession = Pick<AgentSession, "model" | "thinkingLevel
17
17
  setModelTemporary?: AgentSession["setModelTemporary"];
18
18
  setActiveModelProfile?: (name: string | undefined) => void;
19
19
  getActiveModelProfile?: () => string | undefined;
20
+ getSessionDefaultModelSelector?: () => string | undefined;
21
+ recordResumeDefaultModel?: (selector: string) => void;
20
22
  };
21
23
 
22
24
  export interface PrepareModelProfileActivationOptions {
@@ -43,10 +45,20 @@ export interface PreparedModelProfileActivation {
43
45
  previousModel: Model<Api> | undefined;
44
46
  previousThinkingLevel: ThinkingLevel | undefined;
45
47
  previousAgentModelOverrides: Record<string, string>;
48
+ previousModelRoles: Record<string, string>;
46
49
  defaultModel: Model<Api> | undefined;
47
50
  defaultThinkingLevel: ThinkingLevel | undefined;
51
+ modelRoles: Record<string, string>;
48
52
  agentModelOverrides: Record<string, string>;
49
53
  previousActiveModelProfile: string | undefined;
54
+ /**
55
+ * The session resume default ("provider/id") captured BEFORE activation —
56
+ * the model resume would restore prior to this profile. Snapshotted
57
+ * separately from `previousModel` (the live runtime model, which may be a
58
+ * transient switch) so a failed-activation rollback restores the correct
59
+ * resume default without promoting a transient model to it.
60
+ */
61
+ previousSessionDefaultModel: string | undefined;
50
62
  }
51
63
 
52
64
  export function formatModelProfileCredentialError(profileName: string, providers: readonly string[]): string {
@@ -87,14 +99,24 @@ function rewriteSelectorProvider(
87
99
  }
88
100
 
89
101
  function rewriteBindingsProviders(
90
- bindings: { defaultSelector?: string; agentModelOverrides: Record<string, string> },
102
+ bindings: {
103
+ defaultSelector?: string;
104
+ modelRoles: Record<string, string>;
105
+ agentModelOverrides: Record<string, string>;
106
+ },
91
107
  authenticatedProviders: ReadonlySet<string>,
92
108
  alternativeGroups: readonly (readonly string[])[],
93
- ): { defaultSelector?: string; agentModelOverrides: Record<string, string> } {
109
+ ): { defaultSelector?: string; modelRoles: Record<string, string>; agentModelOverrides: Record<string, string> } {
94
110
  return {
95
111
  defaultSelector: bindings.defaultSelector
96
112
  ? rewriteSelectorProvider(bindings.defaultSelector, authenticatedProviders, alternativeGroups)
97
113
  : undefined,
114
+ modelRoles: Object.fromEntries(
115
+ Object.entries(bindings.modelRoles).map(([role, sel]) => [
116
+ role,
117
+ rewriteSelectorProvider(sel, authenticatedProviders, alternativeGroups),
118
+ ]),
119
+ ),
98
120
  agentModelOverrides: Object.fromEntries(
99
121
  Object.entries(bindings.agentModelOverrides).map(([role, sel]) => [
100
122
  role,
@@ -165,6 +187,18 @@ export async function prepareModelProfileActivation(
165
187
  );
166
188
  }
167
189
 
190
+ const modelRoles: Record<string, string> = {};
191
+ for (const [role, selector] of Object.entries(bindings.modelRoles) as [GjcModelAssignmentTargetId, string][]) {
192
+ const resolved = resolveModelRoleValue(selector, availableModels, {
193
+ settings: options.settings as Settings,
194
+ modelRegistry: options.modelRegistry,
195
+ });
196
+ if (!resolved.model) {
197
+ throw new Error(`Model profile "${options.profileName}" ${role} selector did not resolve: ${selector}`);
198
+ }
199
+ modelRoles[role] = formatClampedModelSelector(selector, resolved.model);
200
+ }
201
+
168
202
  const agentModelOverrides: Record<string, string> = {};
169
203
  for (const [role, selector] of Object.entries(bindings.agentModelOverrides) as [
170
204
  GjcModelAssignmentTargetId,
@@ -187,10 +221,13 @@ export async function prepareModelProfileActivation(
187
221
  previousModel: options.session.model,
188
222
  previousThinkingLevel: options.session.thinkingLevel,
189
223
  previousAgentModelOverrides: { ...options.settings.get("task.agentModelOverrides") },
224
+ previousModelRoles: { ...options.settings.get("modelRoles") },
190
225
  defaultModel: resolvedDefault?.model,
191
226
  defaultThinkingLevel: resolvedDefault?.thinkingLevel,
227
+ modelRoles,
192
228
  agentModelOverrides,
193
229
  previousActiveModelProfile: options.session.getActiveModelProfile?.(),
230
+ previousSessionDefaultModel: options.session.getSessionDefaultModelSelector?.(),
194
231
  };
195
232
  }
196
233
 
@@ -201,17 +238,29 @@ export async function applyPreparedModelProfileActivation(
201
238
  const previousModel = prepared.previousModel;
202
239
  const previousThinkingLevel = prepared.previousThinkingLevel;
203
240
  const previousAgentModelOverrides = prepared.previousAgentModelOverrides;
241
+ const previousModelRoles = prepared.previousModelRoles;
204
242
  const previousPersistedDefault = prepared.settings.get("modelProfile.default");
205
243
  const previousActiveModelProfile = prepared.previousActiveModelProfile;
244
+ const previousSessionDefaultModel = prepared.previousSessionDefaultModel;
206
245
  let modelChanged = false;
207
246
  let overridesChanged = false;
208
247
  let defaultChanged = false;
248
+ let modelRolesChanged = false;
209
249
 
210
250
  try {
211
251
  if (prepared.defaultModel) {
212
- await prepared.session.setModelTemporary(prepared.defaultModel, prepared.defaultThinkingLevel);
252
+ await prepared.session.setModelTemporary(prepared.defaultModel, prepared.defaultThinkingLevel, {
253
+ persistAsSessionDefault: true,
254
+ });
213
255
  modelChanged = true;
214
256
  }
257
+ if (Object.keys(prepared.modelRoles).length > 0) {
258
+ prepared.settings.override("modelRoles", {
259
+ ...prepared.settings.get("modelRoles"),
260
+ ...prepared.modelRoles,
261
+ });
262
+ modelRolesChanged = true;
263
+ }
215
264
  if (Object.keys(prepared.agentModelOverrides).length > 0) {
216
265
  prepared.settings.override("task.agentModelOverrides", {
217
266
  ...prepared.settings.get("task.agentModelOverrides"),
@@ -229,12 +278,32 @@ export async function applyPreparedModelProfileActivation(
229
278
  if (defaultChanged) {
230
279
  prepared.settings.set("modelProfile.default", previousPersistedDefault);
231
280
  }
281
+ if (modelRolesChanged) {
282
+ prepared.settings.override("modelRoles", previousModelRoles);
283
+ }
232
284
  if (overridesChanged) {
233
285
  prepared.settings.override("task.agentModelOverrides", previousAgentModelOverrides);
234
286
  }
235
287
  prepared.session.setActiveModelProfile?.(previousActiveModelProfile);
236
- if (modelChanged && previousModel) {
237
- await prepared.session.setModelTemporary(previousModel, previousThinkingLevel);
288
+ if (modelChanged) {
289
+ // Runtime rolls back to the pre-activation live model. That model may
290
+ // itself be a transient retry/fallback/context-promotion/plan switch,
291
+ // so it is recorded as role:"temporary" (NOT the resume default) to
292
+ // preserve the issue #849 protection.
293
+ if (previousModel) {
294
+ await prepared.session.setModelTemporary(previousModel, previousThinkingLevel);
295
+ }
296
+ // The happy path already appended the profile main model as the resume
297
+ // default (role:"default"). Re-assert the pre-activation resume default
298
+ // so a failed activation does not poison future resume. Fall back to the
299
+ // live model only when there was no explicit pre-activation default
300
+ // (nothing to protect). Append-only — never touches the runtime model.
301
+ const restoreDefaultSelector =
302
+ previousSessionDefaultModel ??
303
+ (previousModel ? `${previousModel.provider}/${previousModel.id}` : undefined);
304
+ if (restoreDefaultSelector) {
305
+ prepared.session.recordResumeDefaultModel?.(restoreDefaultSelector);
306
+ }
238
307
  }
239
308
  throw error;
240
309
  }
@@ -23,7 +23,8 @@ export interface ModelProfileDefinition {
23
23
 
24
24
  export interface ResolvedProfileBinding {
25
25
  defaultSelector?: string;
26
- agentModelOverrides: Partial<Record<Exclude<ModelProfileRole, "default">, string>>;
26
+ modelRoles: Partial<Record<"vision", string>>;
27
+ agentModelOverrides: Partial<Record<Exclude<ModelProfileRole, "default" | "vision">, string>>;
27
28
  }
28
29
 
29
30
  function parseModelSelectorProvider(selector: string): string | undefined {
@@ -56,7 +57,7 @@ export function aggregateModelProfileRequiredProviders(
56
57
  const profile = (
57
58
  name: string,
58
59
  requiredProviders: string[],
59
- modelMapping: Record<ModelProfileRole, string>,
60
+ modelMapping: Partial<Record<ModelProfileRole, string>>,
60
61
  alternativeProviderGroups?: readonly (readonly string[])[],
61
62
  ): ModelProfileDefinition => ({
62
63
  name,
@@ -382,13 +383,15 @@ export function mergeModelProfiles(userProfiles?: ModelsConfig["profiles"]): Map
382
383
  }
383
384
 
384
385
  export function resolveProfileBindings(definition: ModelProfileDefinition): ResolvedProfileBinding {
385
- const { default: defaultSelector, executor, architect, planner, critic } = definition.modelMapping;
386
+ const { default: defaultSelector, vision, executor, architect, planner, critic } = definition.modelMapping;
387
+ const modelRoles: ResolvedProfileBinding["modelRoles"] = {};
388
+ if (vision !== undefined) modelRoles.vision = vision;
386
389
  const agentModelOverrides: ResolvedProfileBinding["agentModelOverrides"] = {};
387
390
  if (executor !== undefined) agentModelOverrides.executor = executor;
388
391
  if (architect !== undefined) agentModelOverrides.architect = architect;
389
392
  if (planner !== undefined) agentModelOverrides.planner = planner;
390
393
  if (critic !== undefined) agentModelOverrides.critic = critic;
391
- return { defaultSelector, agentModelOverrides };
394
+ return { defaultSelector, modelRoles, agentModelOverrides };
392
395
  }
393
396
 
394
397
  export function formatAvailableProfileNames(profiles: ReadonlyMap<string, ModelProfileDefinition>): string {