@gajae-code/coding-agent 0.7.7 → 0.7.9

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 (96) hide show
  1. package/CHANGELOG.md +38 -1
  2. package/README.md +14 -0
  3. package/dist/types/cli/local-provider-smoke.d.ts +59 -0
  4. package/dist/types/cli/notify-cli.d.ts +1 -1
  5. package/dist/types/cli/update-cli.d.ts +24 -1
  6. package/dist/types/commands/local-provider.d.ts +34 -0
  7. package/dist/types/config/keybindings.d.ts +1 -1
  8. package/dist/types/config/model-profile-activation.d.ts +19 -2
  9. package/dist/types/config/model-profiles.d.ts +1 -0
  10. package/dist/types/config/model-registry.d.ts +9 -0
  11. package/dist/types/config/models-config-schema.d.ts +5 -0
  12. package/dist/types/config/settings-schema.d.ts +45 -0
  13. package/dist/types/config/settings.d.ts +7 -0
  14. package/dist/types/coordinator-mcp/server.d.ts +8 -0
  15. package/dist/types/gjc-runtime/launch-tmux.d.ts +14 -5
  16. package/dist/types/gjc-runtime/team-runtime.d.ts +1 -1
  17. package/dist/types/gjc-runtime/tmux-common.d.ts +12 -0
  18. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +3 -1
  19. package/dist/types/modes/components/custom-model-preset-wizard.d.ts +1 -1
  20. package/dist/types/modes/components/hook-input.d.ts +1 -0
  21. package/dist/types/modes/components/model-selector.d.ts +11 -0
  22. package/dist/types/modes/components/settings-selector.d.ts +4 -1
  23. package/dist/types/modes/components/status-line/types.d.ts +5 -7
  24. package/dist/types/modes/components/status-line.d.ts +7 -3
  25. package/dist/types/modes/controllers/extension-ui-controller.d.ts +3 -1
  26. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  27. package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
  28. package/dist/types/modes/interactive-mode.d.ts +3 -1
  29. package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -0
  30. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +21 -1
  31. package/dist/types/modes/tmux-scroll.d.ts +18 -0
  32. package/dist/types/modes/types.d.ts +3 -1
  33. package/dist/types/modes/utils/terminal-bell.d.ts +3 -0
  34. package/dist/types/notifications/daemon-paths.d.ts +9 -0
  35. package/dist/types/notifications/telegram-daemon-cli.d.ts +21 -4
  36. package/dist/types/notifications/telegram-daemon.d.ts +1 -10
  37. package/dist/types/tools/ask.d.ts +24 -0
  38. package/dist/types/tools/ultragoal-ask-guard.d.ts +9 -2
  39. package/package.json +7 -7
  40. package/src/cli/local-provider-smoke.ts +632 -0
  41. package/src/cli/notify-cli.ts +5 -3
  42. package/src/cli/update-cli.ts +155 -13
  43. package/src/cli.ts +19 -0
  44. package/src/commands/launch.ts +46 -1
  45. package/src/commands/local-provider.ts +62 -0
  46. package/src/commands/notify.ts +1 -1
  47. package/src/config/keybindings.ts +1 -1
  48. package/src/config/model-profile-activation.ts +92 -11
  49. package/src/config/model-profiles.ts +15 -2
  50. package/src/config/model-registry.ts +136 -6
  51. package/src/config/models-config-schema.ts +9 -0
  52. package/src/config/settings-schema.ts +48 -0
  53. package/src/config/settings.ts +11 -0
  54. package/src/coordinator-mcp/server.ts +280 -36
  55. package/src/deep-interview/render-middleware.ts +1 -0
  56. package/src/defaults/gjc/skills/deep-interview/SKILL.md +14 -6
  57. package/src/defaults/gjc/skills/ralplan/SKILL.md +14 -7
  58. package/src/gjc-runtime/deep-interview-runtime.ts +1 -1
  59. package/src/gjc-runtime/launch-tmux.ts +321 -38
  60. package/src/gjc-runtime/launch-worktree.ts +16 -2
  61. package/src/gjc-runtime/team-runtime.ts +87 -7
  62. package/src/gjc-runtime/tmux-common.ts +16 -0
  63. package/src/gjc-runtime/tmux-sessions.ts +13 -9
  64. package/src/gjc-runtime/ultragoal-guard.ts +11 -5
  65. package/src/internal-urls/docs-index.generated.ts +10 -9
  66. package/src/modes/components/agent-dashboard.ts +2 -1
  67. package/src/modes/components/custom-model-preset-wizard.ts +31 -192
  68. package/src/modes/components/hook-editor.ts +7 -4
  69. package/src/modes/components/hook-input.ts +4 -0
  70. package/src/modes/components/model-selector.ts +260 -12
  71. package/src/modes/components/session-observer-overlay.ts +3 -3
  72. package/src/modes/components/settings-selector.ts +506 -5
  73. package/src/modes/components/status-line/segments.ts +16 -19
  74. package/src/modes/components/status-line/types.ts +6 -2
  75. package/src/modes/components/status-line.ts +70 -35
  76. package/src/modes/controllers/event-controller.ts +98 -4
  77. package/src/modes/controllers/extension-ui-controller.ts +5 -0
  78. package/src/modes/controllers/input-controller.ts +15 -0
  79. package/src/modes/controllers/selector-controller.ts +142 -11
  80. package/src/modes/interactive-mode.ts +7 -2
  81. package/src/modes/prompt-action-autocomplete.ts +8 -0
  82. package/src/modes/shared/agent-wire/deep-interview-gate.ts +11 -4
  83. package/src/modes/tmux-scroll.ts +92 -0
  84. package/src/modes/types.ts +6 -1
  85. package/src/modes/utils/terminal-bell.ts +48 -0
  86. package/src/notifications/daemon-paths.ts +22 -0
  87. package/src/notifications/telegram-daemon-cli.ts +125 -13
  88. package/src/notifications/telegram-daemon.ts +6 -21
  89. package/src/prompts/system/system-prompt.md +5 -0
  90. package/src/sdk.ts +10 -1
  91. package/src/session/agent-session.ts +84 -6
  92. package/src/slash-commands/builtin-registry.ts +106 -13
  93. package/src/tools/ask.ts +11 -1
  94. package/src/tools/eval.ts +2 -2
  95. package/src/tools/render-utils.ts +2 -0
  96. package/src/tools/ultragoal-ask-guard.ts +39 -5
@@ -83,7 +83,8 @@ export const MODEL_ROLES: Record<ModelRole, ModelRoleInfo> = {
83
83
  };
84
84
 
85
85
  export const MODEL_ROLE_IDS: ModelRole[] = ["default"];
86
-
86
+ export const MODEL_PROFILE_NAME_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
87
+ export const MODEL_PROFILE_NAME_PATTERN_DESCRIPTION = "lowercase letters, numbers, dots, underscores, or hyphens";
87
88
  export type GjcModelAssignmentTargetId = "default" | "executor" | "architect" | "planner" | "critic";
88
89
 
89
90
  export interface GjcModelAssignmentTargetInfo extends ModelRoleInfo {
@@ -243,6 +244,7 @@ interface ProviderValidationConfig {
243
244
  requestTransform?: ModelRequestTransform;
244
245
  disableStrictTools?: boolean;
245
246
  cacheRetention?: CacheRetention;
247
+ openaiCompat?: { baseUrl: string; apiKey?: string; apiKeyEnv?: string };
246
248
  modelOverrides?: Record<string, unknown>;
247
249
  models: ProviderValidationModel[];
248
250
  }
@@ -271,11 +273,12 @@ function validateProviderConfiguration(
271
273
  !config.disableStrictTools &&
272
274
  !config.requestTransform &&
273
275
  !config.cacheRetention &&
276
+ !config.openaiCompat &&
274
277
  !hasModelOverrides &&
275
278
  !config.discovery
276
279
  ) {
277
280
  throw new Error(
278
- `Provider ${providerName}: must specify "baseUrl", "headers", "apiKey", "compat", "requestTransform", "cacheRetention", "disableStrictTools", "modelOverrides", "discovery", or "models"`,
281
+ `Provider ${providerName}: must specify "baseUrl", "headers", "apiKey", "compat", "requestTransform", "cacheRetention", "disableStrictTools", "modelOverrides", "discovery", "openaiCompat", or "models"`,
279
282
  );
280
283
  }
281
284
  }
@@ -389,6 +392,7 @@ export const ModelsConfigFile = new ConfigFile<ModelsConfig>("models", ModelsCon
389
392
  requestTransform: providerConfig.requestTransform,
390
393
  disableStrictTools: providerConfig.disableStrictTools,
391
394
  cacheRetention: providerConfig.cacheRetention,
395
+ openaiCompat: providerConfig.openaiCompat,
392
396
  modelOverrides: providerConfig.modelOverrides,
393
397
  models: (providerConfig.models ?? []) as ProviderValidationModel[],
394
398
  },
@@ -436,6 +440,18 @@ function resolveProviderBaseUrlFromEnv(provider: string): string | undefined {
436
440
  return $pickenv(...getProviderBaseUrlEnvKeys(provider));
437
441
  }
438
442
 
443
+ function normalizeLocalOpenAICompatBaseUrl(baseUrl: string): string {
444
+ try {
445
+ const parsed = new URL(baseUrl);
446
+ const trimmedPath = parsed.pathname.replace(/\/+$/g, "");
447
+ parsed.pathname = trimmedPath.endsWith("/v1") ? trimmedPath || "/v1" : `${trimmedPath}/v1`;
448
+ return `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
449
+ } catch {
450
+ const trimmed = baseUrl.replace(/\/+$/g, "");
451
+ return trimmed.endsWith("/v1") ? trimmed : `${trimmed}/v1`;
452
+ }
453
+ }
454
+
439
455
  /**
440
456
  * Merge a freshly discovered model with the matching bundled/configured entry
441
457
  * (or a runtime provider override when no bundled entry exists).
@@ -1409,6 +1425,45 @@ export class ModelRegistry {
1409
1425
  for (const [providerName, providerConfig] of providerEntries) {
1410
1426
  if (providerConfig.webSearch) this.#providerWebSearchModes.set(providerName, providerConfig.webSearch);
1411
1427
  const providerApiKeyConfig = providerConfig.apiKey ?? resolveApiKeyEnvConfig(providerConfig.apiKeyEnv);
1428
+ const localOpenAICompat = providerConfig.openaiCompat;
1429
+ const localOpenAICompatApiKeyConfig = localOpenAICompat
1430
+ ? (localOpenAICompat.apiKey ?? resolveApiKeyEnvConfig(localOpenAICompat.apiKeyEnv))
1431
+ : undefined;
1432
+ if (localOpenAICompat) {
1433
+ const localOpenAICompatBaseUrl = normalizeLocalOpenAICompatBaseUrl(localOpenAICompat.baseUrl);
1434
+ const localCompatResolvedKey = localOpenAICompat.apiKeyEnv
1435
+ ? resolveApiKeyEnvConfig(localOpenAICompat.apiKeyEnv)
1436
+ : localOpenAICompat.apiKey
1437
+ ? resolveApiKeyConfig(localOpenAICompat.apiKey)
1438
+ : undefined;
1439
+ overrides.set(providerName, {
1440
+ baseUrl: localOpenAICompatBaseUrl,
1441
+ apiKey: localOpenAICompatApiKeyConfig,
1442
+ compat: {
1443
+ supportsStore: false,
1444
+ supportsDeveloperRole: false,
1445
+ supportsReasoningEffort: false,
1446
+ },
1447
+ });
1448
+ discoverableProviders.push({
1449
+ provider: providerName,
1450
+ api: "openai-completions",
1451
+ baseUrl: localOpenAICompatBaseUrl,
1452
+ compat: {
1453
+ supportsStore: false,
1454
+ supportsDeveloperRole: false,
1455
+ supportsReasoningEffort: false,
1456
+ },
1457
+ discovery: { type: "openai-models-list" },
1458
+ optional: false,
1459
+ });
1460
+ if (localCompatResolvedKey) {
1461
+ this.#customProviderApiKeys.set(providerName, localCompatResolvedKey);
1462
+ this.authStorage.setConfigApiKey(providerName, localCompatResolvedKey);
1463
+ } else {
1464
+ keylessProviders.add(providerName);
1465
+ }
1466
+ }
1412
1467
  // Always set overrides when baseUrl/headers/apiKey/authHeader/compat/disableStrictTools/transport are present
1413
1468
  if (
1414
1469
  providerConfig.baseUrl ||
@@ -1536,10 +1591,7 @@ export class ModelRegistry {
1536
1591
  const where = first?.path.length ? `/${first.path.map(String).join("/")}` : "root";
1537
1592
  throw new Error(`Generated models config is invalid at ${where}: ${first?.message ?? "unknown schema error"}`);
1538
1593
  }
1539
- await fs.mkdir(path.dirname(this.#modelsConfigFile.path()), { recursive: true });
1540
- await Bun.write(this.#modelsConfigFile.path(), Bun.YAML.stringify(checkedConfig.data, null, 2));
1541
- this.#modelsConfigFile.invalidate();
1542
- this.#reloadStaticModels();
1594
+ await this.#writeCheckedModelsConfig(checkedConfig.data);
1543
1595
  const modelMapping = { ...definition.model_mapping };
1544
1596
  const profile: ModelProfileDefinition = {
1545
1597
  name: normalizedName,
@@ -1550,6 +1602,84 @@ export class ModelRegistry {
1550
1602
  };
1551
1603
  return profile;
1552
1604
  }
1605
+
1606
+ async renameCustomModelProfile(name: string, displayName: string): Promise<ModelProfileDefinition> {
1607
+ const normalizedName = name.trim();
1608
+ const nextDisplayName = displayName.trim();
1609
+ if (!normalizedName) throw new Error("Profile name is required.");
1610
+ if (!nextDisplayName) throw new Error("Profile display name is required.");
1611
+ const { current, profile } = this.#loadCustomProfileForMutation(normalizedName, "rename");
1612
+ const nextProfiles = {
1613
+ ...(current.profiles ?? {}),
1614
+ [normalizedName]: {
1615
+ ...profile,
1616
+ display_name: nextDisplayName,
1617
+ },
1618
+ };
1619
+ const checkedConfig = ModelsConfigSchema.safeParse({ ...current, profiles: nextProfiles });
1620
+ if (!checkedConfig.success) {
1621
+ const first = checkedConfig.error.issues[0];
1622
+ const where = first?.path.length ? `/${first.path.map(String).join("/")}` : "root";
1623
+ throw new Error(`Generated models config is invalid at ${where}: ${first?.message ?? "unknown schema error"}`);
1624
+ }
1625
+ await this.#writeCheckedModelsConfig(checkedConfig.data);
1626
+ const modelMapping = { ...profile.model_mapping };
1627
+ return {
1628
+ name: normalizedName,
1629
+ displayName: nextDisplayName,
1630
+ requiredProviders: aggregateModelProfileRequiredProviders(profile.required_providers, { modelMapping }),
1631
+ modelMapping,
1632
+ source: "user",
1633
+ };
1634
+ }
1635
+
1636
+ async deleteCustomModelProfile(name: string): Promise<ModelProfileConfig> {
1637
+ const normalizedName = name.trim();
1638
+ if (!normalizedName) throw new Error("Profile name is required.");
1639
+ const { current, profile } = this.#loadCustomProfileForMutation(normalizedName, "delete");
1640
+ const nextProfiles = { ...(current.profiles ?? {}) };
1641
+ delete nextProfiles[normalizedName];
1642
+ const checkedConfig = ModelsConfigSchema.safeParse({
1643
+ ...current,
1644
+ profiles: Object.keys(nextProfiles).length > 0 ? nextProfiles : undefined,
1645
+ });
1646
+ if (!checkedConfig.success) {
1647
+ const first = checkedConfig.error.issues[0];
1648
+ const where = first?.path.length ? `/${first.path.map(String).join("/")}` : "root";
1649
+ throw new Error(`Generated models config is invalid at ${where}: ${first?.message ?? "unknown schema error"}`);
1650
+ }
1651
+ await this.#writeCheckedModelsConfig(checkedConfig.data);
1652
+ return profile;
1653
+ }
1654
+
1655
+ #loadCustomProfileForMutation(
1656
+ normalizedName: string,
1657
+ action: "rename" | "delete",
1658
+ ): { current: ModelsConfig; profile: ModelProfileConfig } {
1659
+ const loaded = this.#modelsConfigFile.tryLoad();
1660
+ if (loaded.status === "error") {
1661
+ throw new Error(
1662
+ `Cannot ${action} custom model profile because ${this.#modelsConfigFile.path()} is invalid. Fix the existing config before modifying presets.`,
1663
+ );
1664
+ }
1665
+ const current = loaded.value ?? this.#modelsConfigFile.createDefault();
1666
+ const profile = current.profiles?.[normalizedName];
1667
+ if (!profile) {
1668
+ const existing = this.#modelProfiles.get(normalizedName);
1669
+ if (existing && existing.source !== "user") {
1670
+ throw new Error(`Cannot ${action} bundled model profile: ${normalizedName}.`);
1671
+ }
1672
+ throw new Error(`Custom model profile does not exist: ${normalizedName}.`);
1673
+ }
1674
+ return { current, profile };
1675
+ }
1676
+
1677
+ async #writeCheckedModelsConfig(config: ModelsConfig): Promise<void> {
1678
+ await fs.mkdir(path.dirname(this.#modelsConfigFile.path()), { recursive: true });
1679
+ await Bun.write(this.#modelsConfigFile.path(), Bun.YAML.stringify(config, null, 2));
1680
+ this.#modelsConfigFile.invalidate();
1681
+ this.#reloadStaticModels();
1682
+ }
1553
1683
  applyConfiguredModelBindings(targetSettings: Settings): void {
1554
1684
  this.#modelBindingsTargetSettings = targetSettings;
1555
1685
  this.#applyConfiguredModelBindingsToTarget();
@@ -193,6 +193,14 @@ export const ProviderDiscoverySchema = z.object({
193
193
  type: z.enum(["ollama", "llama.cpp", "lm-studio", "openai-models-list"]),
194
194
  });
195
195
 
196
+ const LocalOpenAICompatSchema = z
197
+ .object({
198
+ baseUrl: z.string().min(1),
199
+ apiKey: z.string().min(1).optional(),
200
+ apiKeyEnv: z.string().min(1).optional(),
201
+ })
202
+ .strict();
203
+
196
204
  export const ProviderAuthSchema = z.enum(["apiKey", "none", "oauth"]);
197
205
 
198
206
  export type ProviderAuthMode = z.infer<typeof ProviderAuthSchema>;
@@ -237,6 +245,7 @@ const ProviderConfigSchema = z
237
245
  */
238
246
  transport: z.literal("pi-native").optional(),
239
247
  cacheRetention: CacheRetentionSchema.optional(),
248
+ openaiCompat: LocalOpenAICompatSchema.optional(),
240
249
  })
241
250
  .strict();
242
251
 
@@ -274,6 +274,44 @@ export const SETTINGS_SCHEMA = {
274
274
  validate: (value: number) => Number.isFinite(value) && value > 0,
275
275
  },
276
276
 
277
+ "notifications.terminalBell": {
278
+ type: "boolean",
279
+ default: false,
280
+ ui: {
281
+ tab: "interaction",
282
+ label: "Terminal Bell",
283
+ description:
284
+ "Emit a BEL character for local terminal notifications. Windows Terminal may keep BEL silent depending on profile/system sound settings; use completion.notifyCommand for a PowerShell Console.Beep workaround.",
285
+ },
286
+ },
287
+ "notifications.bellOnComplete": {
288
+ type: "boolean",
289
+ default: true,
290
+ ui: {
291
+ tab: "interaction",
292
+ label: "Bell on Completion",
293
+ description: "Ring the terminal bell when an agent turn completes",
294
+ },
295
+ },
296
+ "notifications.bellOnApproval": {
297
+ type: "boolean",
298
+ default: true,
299
+ ui: {
300
+ tab: "interaction",
301
+ label: "Bell on Approval",
302
+ description: "Ring the terminal bell when a plan or approval prompt needs attention",
303
+ },
304
+ },
305
+ "notifications.bellOnAsk": {
306
+ type: "boolean",
307
+ default: true,
308
+ ui: {
309
+ tab: "interaction",
310
+ label: "Bell on Ask",
311
+ description: "Ring the terminal bell when an ask/user-input prompt needs attention",
312
+ },
313
+ },
314
+
277
315
  autoResume: {
278
316
  type: "boolean",
279
317
  default: false,
@@ -1135,6 +1173,16 @@ export const SETTINGS_SCHEMA = {
1135
1173
  default: "on",
1136
1174
  ui: { tab: "interaction", label: "Completion Notification", description: "Notify when the agent completes" },
1137
1175
  },
1176
+ "completion.notifyCommand": {
1177
+ type: "string",
1178
+ default: "",
1179
+ ui: {
1180
+ tab: "interaction",
1181
+ label: "Completion Notification Command",
1182
+ description:
1183
+ "Optional user-level shell command to run when an agent turn completes; receives GJC_NOTIFICATION_* environment variables. On Windows, this can call PowerShell [Console]::Beep when terminal BEL is silent.",
1184
+ },
1185
+ },
1138
1186
 
1139
1187
  "ask.timeout": {
1140
1188
  type: "number",
@@ -297,6 +297,17 @@ export class Settings {
297
297
  return getDefault(path);
298
298
  }
299
299
 
300
+ /**
301
+ * Get a setting value from the user/global config only.
302
+ *
303
+ * Use for machine-local command hooks and other settings that must not be
304
+ * activated by project-scoped config files.
305
+ */
306
+ getGlobal<P extends SettingPath>(path: P): SettingValue<P> | undefined {
307
+ const value = getByPath(this.#global, path.split("."));
308
+ return value === undefined ? undefined : (value as SettingValue<P>);
309
+ }
310
+
300
311
  /** Check whether a setting is present in loaded settings/overrides rather than coming from schema defaults. */
301
312
  has(path: SettingPath): boolean {
302
313
  return getByPath(this.#merged, path.split(".")) !== undefined;