@gajae-code/coding-agent 0.2.1 → 0.2.3

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 (153) hide show
  1. package/CHANGELOG.md +59 -1
  2. package/dist/types/cli/setup-cli.d.ts +1 -0
  3. package/dist/types/commands/contribution-prep.d.ts +18 -0
  4. package/dist/types/commands/deep-interview.d.ts +41 -0
  5. package/dist/types/commands/session.d.ts +24 -0
  6. package/dist/types/commands/setup.d.ts +3 -0
  7. package/dist/types/config/model-registry.d.ts +2 -2
  8. package/dist/types/config/models-config-schema.d.ts +17 -9
  9. package/dist/types/config/settings-schema.d.ts +37 -24
  10. package/dist/types/discovery/helpers.d.ts +2 -0
  11. package/dist/types/extensibility/extensions/types.d.ts +6 -0
  12. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +33 -0
  13. package/dist/types/gjc-runtime/goal-mode-request.d.ts +1 -1
  14. package/dist/types/gjc-runtime/launch-tmux.d.ts +12 -11
  15. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +25 -0
  16. package/dist/types/gjc-runtime/state-runtime.d.ts +13 -0
  17. package/dist/types/gjc-runtime/team-runtime.d.ts +37 -5
  18. package/dist/types/gjc-runtime/tmux-common.d.ts +41 -0
  19. package/dist/types/gjc-runtime/tmux-sessions.d.ts +17 -0
  20. package/dist/types/goals/runtime.d.ts +3 -9
  21. package/dist/types/goals/state.d.ts +3 -6
  22. package/dist/types/goals/tools/goal-tool.d.ts +1 -69
  23. package/dist/types/hooks/skill-state.d.ts +5 -0
  24. package/dist/types/memories/index.d.ts +1 -1
  25. package/dist/types/memory-backend/local-backend.d.ts +3 -3
  26. package/dist/types/modes/components/hook-selector.d.ts +7 -0
  27. package/dist/types/modes/components/settings-selector.d.ts +0 -2
  28. package/dist/types/modes/components/status-line/types.d.ts +0 -3
  29. package/dist/types/modes/components/status-line.d.ts +0 -3
  30. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  31. package/dist/types/modes/interactive-mode.d.ts +1 -12
  32. package/dist/types/modes/theme/defaults/index.d.ts +0 -2
  33. package/dist/types/modes/theme/theme.d.ts +1 -2
  34. package/dist/types/modes/types.d.ts +1 -7
  35. package/dist/types/modes/utils/context-usage.d.ts +6 -2
  36. package/dist/types/sdk.d.ts +6 -2
  37. package/dist/types/session/agent-session.d.ts +47 -1
  38. package/dist/types/session/contribution-prep.d.ts +47 -0
  39. package/dist/types/session/session-manager.d.ts +3 -0
  40. package/dist/types/setup/model-onboarding-guidance.d.ts +1 -0
  41. package/dist/types/setup/provider-onboarding.d.ts +29 -5
  42. package/dist/types/skill-state/active-state.d.ts +30 -1
  43. package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +6 -1
  44. package/dist/types/skill-state/initial-phase.d.ts +12 -0
  45. package/dist/types/skill-state/workflow-hud.d.ts +9 -4
  46. package/dist/types/skill-state/workflow-state-contract.d.ts +34 -0
  47. package/dist/types/task/executor.d.ts +2 -0
  48. package/dist/types/task/types.d.ts +11 -0
  49. package/dist/types/tools/index.d.ts +20 -1
  50. package/dist/types/tools/skill.d.ts +47 -0
  51. package/dist/types/utils/changelog.d.ts +18 -2
  52. package/package.json +7 -7
  53. package/src/cli/args.ts +3 -2
  54. package/src/cli/setup-cli.ts +26 -12
  55. package/src/cli.ts +7 -1
  56. package/src/commands/contribution-prep.ts +41 -0
  57. package/src/commands/deep-interview.ts +30 -23
  58. package/src/commands/launch.ts +10 -1
  59. package/src/commands/ralplan.ts +10 -22
  60. package/src/commands/session.ts +150 -0
  61. package/src/commands/setup.ts +2 -0
  62. package/src/commands/state.ts +15 -4
  63. package/src/commands/team.ts +23 -3
  64. package/src/config/model-registry.ts +10 -2
  65. package/src/config/models-config-schema.ts +120 -102
  66. package/src/config/settings-schema.ts +42 -25
  67. package/src/config.ts +1 -1
  68. package/src/defaults/gjc/skills/deep-interview/SKILL.md +32 -13
  69. package/src/defaults/gjc/skills/ralplan/SKILL.md +22 -2
  70. package/src/defaults/gjc/skills/team/SKILL.md +39 -7
  71. package/src/defaults/gjc/skills/ultragoal/SKILL.md +33 -25
  72. package/src/discovery/helpers.ts +24 -1
  73. package/src/eval/py/prelude.py +1 -1
  74. package/src/extensibility/extensions/types.ts +6 -0
  75. package/src/gjc-runtime/deep-interview-runtime.ts +546 -0
  76. package/src/gjc-runtime/goal-mode-request.ts +2 -19
  77. package/src/gjc-runtime/launch-tmux.ts +83 -43
  78. package/src/gjc-runtime/ralplan-runtime.ts +460 -0
  79. package/src/gjc-runtime/state-runtime.ts +731 -0
  80. package/src/gjc-runtime/team-runtime.ts +708 -52
  81. package/src/gjc-runtime/tmux-common.ts +119 -0
  82. package/src/gjc-runtime/tmux-sessions.ts +165 -0
  83. package/src/gjc-runtime/ultragoal-guard.ts +6 -3
  84. package/src/gjc-runtime/ultragoal-runtime.ts +5 -4
  85. package/src/goals/runtime.ts +38 -144
  86. package/src/goals/state.ts +36 -7
  87. package/src/goals/tools/goal-tool.ts +15 -172
  88. package/src/hooks/skill-state.ts +39 -18
  89. package/src/internal-urls/docs-index.generated.ts +5 -4
  90. package/src/internal-urls/memory-protocol.ts +3 -2
  91. package/src/main.ts +2 -3
  92. package/src/memories/index.ts +2 -1
  93. package/src/memory-backend/local-backend.ts +14 -6
  94. package/src/modes/components/hook-selector.ts +156 -1
  95. package/src/modes/components/settings-selector.ts +5 -12
  96. package/src/modes/components/skill-hud/render.ts +4 -0
  97. package/src/modes/components/status-line/segments.ts +5 -16
  98. package/src/modes/components/status-line/types.ts +0 -3
  99. package/src/modes/components/status-line.ts +0 -6
  100. package/src/modes/controllers/command-controller.ts +27 -4
  101. package/src/modes/controllers/extension-ui-controller.ts +1 -0
  102. package/src/modes/controllers/input-controller.ts +0 -15
  103. package/src/modes/controllers/selector-controller.ts +4 -11
  104. package/src/modes/interactive-mode.ts +18 -219
  105. package/src/modes/theme/defaults/dark-poimandres.json +0 -1
  106. package/src/modes/theme/defaults/light-poimandres.json +0 -1
  107. package/src/modes/theme/theme.ts +0 -6
  108. package/src/modes/types.ts +1 -7
  109. package/src/modes/utils/context-usage.ts +66 -17
  110. package/src/prompts/agents/architect.md +3 -0
  111. package/src/prompts/agents/executor.md +2 -0
  112. package/src/prompts/agents/frontmatter.md +1 -0
  113. package/src/prompts/goals/goal-continuation.md +1 -4
  114. package/src/prompts/goals/goal-mode-active.md +3 -5
  115. package/src/prompts/system/subagent-system-prompt.md +6 -0
  116. package/src/prompts/system/system-prompt.md +5 -7
  117. package/src/prompts/tools/goal.md +4 -4
  118. package/src/prompts/tools/skill.md +28 -0
  119. package/src/prompts/tools/task.md +3 -0
  120. package/src/sdk.ts +51 -11
  121. package/src/session/agent-session.ts +222 -21
  122. package/src/session/contribution-prep.ts +320 -0
  123. package/src/session/session-manager.ts +9 -1
  124. package/src/setup/model-onboarding-guidance.ts +6 -3
  125. package/src/setup/provider-onboarding.ts +177 -16
  126. package/src/skill-state/active-state.ts +188 -25
  127. package/src/skill-state/deep-interview-mutation-guard.ts +72 -21
  128. package/src/skill-state/initial-phase.ts +17 -0
  129. package/src/skill-state/workflow-hud.ts +23 -5
  130. package/src/skill-state/workflow-state-contract.ts +121 -0
  131. package/src/slash-commands/builtin-registry.ts +75 -25
  132. package/src/slash-commands/helpers/context-report.ts +123 -13
  133. package/src/task/agents.ts +1 -0
  134. package/src/task/commands.ts +1 -5
  135. package/src/task/executor.ts +9 -1
  136. package/src/task/index.ts +91 -4
  137. package/src/task/types.ts +6 -0
  138. package/src/tools/ask.ts +2 -0
  139. package/src/tools/gh.ts +212 -2
  140. package/src/tools/index.ts +25 -6
  141. package/src/tools/skill.ts +153 -0
  142. package/src/utils/changelog.ts +67 -44
  143. package/dist/types/commands/gjc-runtime-bridge.d.ts +0 -30
  144. package/dist/types/commands/question.d.ts +0 -7
  145. package/dist/types/modes/loop-limit.d.ts +0 -22
  146. package/src/commands/gjc-runtime-bridge.ts +0 -227
  147. package/src/commands/question.ts +0 -12
  148. package/src/modes/loop-limit.ts +0 -140
  149. package/src/prompts/commands/orchestrate.md +0 -49
  150. package/src/prompts/goals/goal-budget-limit.md +0 -16
  151. package/src/prompts/tools/create-goal.md +0 -3
  152. package/src/prompts/tools/get-goal.md +0 -3
  153. package/src/prompts/tools/update-goal.md +0 -3
@@ -0,0 +1,320 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import type { AgentMessage } from "@gajae-code/agent-core";
5
+ import type { AssistantMessage, ToolResultMessage, UserMessage } from "@gajae-code/ai";
6
+ import { $ } from "bun";
7
+ import { resolveGjcCommand } from "../task/gjc-command";
8
+ import { shortenPath } from "../tools/render-utils";
9
+
10
+ export const CONTRIBUTION_PREP_SCHEMA_VERSION = 1;
11
+
12
+ const MAX_TRANSCRIPT_MESSAGES = 20;
13
+ const MAX_TEXT_CHARS = 12000;
14
+ const MAX_GIT_OUTPUT_CHARS = 60000;
15
+
16
+ export interface ContributionPrepArtifact {
17
+ path: string;
18
+ description: string;
19
+ }
20
+
21
+ export interface ContributionPrepManifest {
22
+ schema_version: number;
23
+ source_session_id: string;
24
+ created_at: string;
25
+ cwd: string;
26
+ git_head: string | null;
27
+ changed_files: string[];
28
+ artifacts: ContributionPrepArtifact[];
29
+ redactions: string[];
30
+ recommended_output: string[];
31
+ worker_prompt_path: string;
32
+ }
33
+
34
+ export interface ContributionPrepResult {
35
+ manifestPath: string;
36
+ workerPromptPath: string;
37
+ artifactDir: string;
38
+ changedFiles: string[];
39
+ spawned: boolean;
40
+ }
41
+
42
+ export interface ContributionPrepOptions {
43
+ customInstructions?: string;
44
+ spawnWorker?: boolean;
45
+ artifactRoot?: string;
46
+ now?: Date;
47
+ spawn?: (args: string[], cwd: string, shell: boolean) => Promise<void>;
48
+ }
49
+
50
+ export interface ContributionPrepContext {
51
+ sessionId: string;
52
+ cwd: string;
53
+ sessionFile?: string;
54
+ messages: AgentMessage[];
55
+ customInstructions?: string;
56
+ now?: Date;
57
+ }
58
+
59
+ interface RedactionState {
60
+ labels: Set<string>;
61
+ }
62
+
63
+ function limitText(text: string, maxChars = MAX_TEXT_CHARS): string {
64
+ if (text.length <= maxChars) return text;
65
+ return `${text.slice(0, maxChars)}\n\n[truncated ${text.length - maxChars} chars]`;
66
+ }
67
+
68
+ function replaceRegex(text: string, regex: RegExp, replacement: string, state: RedactionState, label: string): string {
69
+ if (!regex.test(text)) return text;
70
+ state.labels.add(label);
71
+ regex.lastIndex = 0;
72
+ return text.replace(regex, replacement);
73
+ }
74
+
75
+ export function redactContributionPrepText(
76
+ text: string,
77
+ cwd: string,
78
+ state: RedactionState = { labels: new Set() },
79
+ ): string {
80
+ let redacted = text;
81
+ redacted = replaceRegex(
82
+ redacted,
83
+ /\b(?:sk|pk|rk|xox[baprs])-[-_A-Za-z0-9]{12,}\b/g,
84
+ "[REDACTED_TOKEN]",
85
+ state,
86
+ "tokens",
87
+ );
88
+ redacted = replaceRegex(
89
+ redacted,
90
+ /\b(?:ghp_[A-Za-z0-9_]{12,}|gho_[A-Za-z0-9_]{12,}|github_pat_[A-Za-z0-9_]{12,})\b/g,
91
+ "[REDACTED_TOKEN]",
92
+ state,
93
+ "tokens",
94
+ );
95
+ redacted = replaceRegex(
96
+ redacted,
97
+ /\b((?:ANTHROPIC|OPENAI|GITHUB|GOOGLE|GEMINI|KAGI|TAVILY|EXA|PERPLEXITY|ZAI|KIMI|BRAVE|SEARXNG|AWS)_[A-Z0-9_]*(?:KEY|TOKEN|SECRET|COOKIE|PASSWORD))\s*=\s*[^\s\n]+/gi,
98
+ "$1=[REDACTED_SECRET]",
99
+ state,
100
+ "provider_keys",
101
+ );
102
+ redacted = replaceRegex(
103
+ redacted,
104
+ /\b(Authorization|Proxy-Authorization)\s*:\s*(?:Bearer|Basic|Token)\s+[^\s\n]+/gi,
105
+ "$1: [REDACTED_AUTH_HEADER]",
106
+ state,
107
+ "auth_headers",
108
+ );
109
+ redacted = replaceRegex(redacted, /\b(Cookie|Set-Cookie)\s*:\s*[^\n]+/gi, "$1: [REDACTED_COOKIE]", state, "cookies");
110
+ redacted = replaceRegex(
111
+ redacted,
112
+ /https?:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0|10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2\d|3[0-1])\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3})[^\s)>'"]*/gi,
113
+ "[REDACTED_PRIVATE_ENDPOINT]",
114
+ state,
115
+ "private_endpoints",
116
+ );
117
+ const home = os.homedir();
118
+ if (home && redacted.includes(home)) {
119
+ state.labels.add("home_paths");
120
+ redacted = redacted.split(home).join("~");
121
+ }
122
+ const normalizedCwd = path.resolve(cwd);
123
+ if (normalizedCwd && redacted.includes(normalizedCwd)) {
124
+ state.labels.add("cwd_paths");
125
+ redacted = redacted.split(normalizedCwd).join(shortenPath(normalizedCwd));
126
+ }
127
+ return redacted;
128
+ }
129
+
130
+ function contentText(content: UserMessage["content"] | AssistantMessage["content"]): string {
131
+ if (typeof content === "string") return content;
132
+ return content
133
+ .map(part => {
134
+ if (part.type === "text") return part.text;
135
+ if (part.type === "toolCall") return `[tool call: ${part.name}] ${JSON.stringify(part.arguments)}`;
136
+ if (part.type === "image") return "[image]";
137
+ return `[${part.type}]`;
138
+ })
139
+ .join("\n");
140
+ }
141
+
142
+ function formatMessage(message: AgentMessage): string {
143
+ if (message.role === "user" || message.role === "assistant") {
144
+ return `## ${message.role}\n\n${contentText(message.content)}\n`;
145
+ }
146
+ if (message.role === "toolResult") {
147
+ const tool = message as ToolResultMessage;
148
+ return `## toolResult: ${tool.toolName}\n\n${typeof tool.content === "string" ? tool.content : JSON.stringify(tool.content)}\n`;
149
+ }
150
+ return `## ${message.role}\n\n${JSON.stringify(message)}\n`;
151
+ }
152
+
153
+ async function gitOutput(cwd: string, args: string[], maxChars = MAX_GIT_OUTPUT_CHARS): Promise<string> {
154
+ try {
155
+ const output = await $`git ${args}`.cwd(cwd).quiet().text();
156
+ return limitText(output.trim(), maxChars);
157
+ } catch {
158
+ return "";
159
+ }
160
+ }
161
+
162
+ async function changedFiles(cwd: string): Promise<string[]> {
163
+ const output = await gitOutput(cwd, ["status", "--short"]);
164
+ return output
165
+ .split("\n")
166
+ .map(line => line.trim())
167
+ .filter(Boolean)
168
+ .map(line => line.replace(/^..\s+/, ""));
169
+ }
170
+
171
+ async function writeArtifact(
172
+ dir: string,
173
+ name: string,
174
+ description: string,
175
+ text: string,
176
+ ): Promise<ContributionPrepArtifact> {
177
+ const filePath = path.join(dir, name);
178
+ await Bun.write(filePath, `${text.trimEnd()}\n`);
179
+ return { path: filePath, description };
180
+ }
181
+
182
+ export function buildContributionPrepWorkerPrompt(manifestPath: string): string {
183
+ return [
184
+ "Prepare a maintainer-friendly contribution draft from the redacted context dump.",
185
+ "Read the manifest and referenced artifact file pointers. Do not assume transcript context was inlined here.",
186
+ `Manifest: ${manifestPath}`,
187
+ "Produce structured markdown with: title, problem summary, reproduction/context, proposed fix or implementation plan, affected files, tests to run, and uncertainty/remaining risks.",
188
+ "Do not create GitHub issues, open PRs, push branches, or perform remote writes unless the user explicitly confirms that action in this fresh session.",
189
+ ].join("\n");
190
+ }
191
+
192
+ export async function prepareContributionPrep(
193
+ context: ContributionPrepContext,
194
+ options: ContributionPrepOptions = {},
195
+ ): Promise<ContributionPrepResult> {
196
+ const createdAt = (options.now ?? context.now ?? new Date()).toISOString();
197
+ const safeTimestamp = createdAt.replace(/[:.]/g, "-");
198
+ const artifactDir = path.join(
199
+ options.artifactRoot ?? path.join(context.cwd, ".gjc", "contribution-prep"),
200
+ safeTimestamp,
201
+ );
202
+ await fs.mkdir(artifactDir, { recursive: true });
203
+
204
+ const redactions: RedactionState = { labels: new Set() };
205
+ const recentMessages = context.messages.slice(-MAX_TRANSCRIPT_MESSAGES);
206
+ const artifacts: ContributionPrepArtifact[] = [];
207
+ const redact = (text: string) => redactContributionPrepText(text, context.cwd, redactions);
208
+
209
+ artifacts.push(
210
+ await writeArtifact(
211
+ artifactDir,
212
+ "transcript.md",
213
+ "Redacted recent transcript window",
214
+ redact(recentMessages.map(formatMessage).join("\n---\n")),
215
+ ),
216
+ );
217
+ artifacts.push(
218
+ await writeArtifact(
219
+ artifactDir,
220
+ "summary.md",
221
+ "Current session summary and operator instructions",
222
+ redact(
223
+ [
224
+ `# Contribution prep context`,
225
+ `Source session: ${context.sessionId}`,
226
+ `Session file: ${context.sessionFile ?? "(none)"}`,
227
+ `Working directory: ${context.cwd}`,
228
+ options.customInstructions || context.customInstructions
229
+ ? `Custom instructions: ${options.customInstructions ?? context.customInstructions}`
230
+ : "Custom instructions: (none)",
231
+ ].join("\n"),
232
+ ),
233
+ ),
234
+ );
235
+
236
+ const gitHead = (await gitOutput(context.cwd, ["rev-parse", "HEAD"])) || null;
237
+ const files = await changedFiles(context.cwd);
238
+ artifacts.push(
239
+ await writeArtifact(artifactDir, "changed-files.txt", "Changed files from git status", redact(files.join("\n"))),
240
+ );
241
+ artifacts.push(
242
+ await writeArtifact(
243
+ artifactDir,
244
+ "git-diff.patch",
245
+ "Bounded redacted git diff",
246
+ redact(await gitOutput(context.cwd, ["diff", "--no-ext-diff"])),
247
+ ),
248
+ );
249
+ artifacts.push(
250
+ await writeArtifact(
251
+ artifactDir,
252
+ "environment.md",
253
+ "Redacted environment and reproduction metadata",
254
+ redact(
255
+ [
256
+ `cwd: ${context.cwd}`,
257
+ `git_head: ${gitHead ?? "unknown"}`,
258
+ `platform: ${process.platform}`,
259
+ `arch: ${process.arch}`,
260
+ `bun: ${Bun.version}`,
261
+ ].join("\n"),
262
+ ),
263
+ ),
264
+ );
265
+
266
+ const manifestPath = path.join(artifactDir, "manifest.json");
267
+ const workerPromptPath = path.join(artifactDir, "worker-prompt.md");
268
+ await Bun.write(workerPromptPath, `${buildContributionPrepWorkerPrompt(manifestPath)}\n`);
269
+
270
+ const manifest: ContributionPrepManifest = {
271
+ schema_version: CONTRIBUTION_PREP_SCHEMA_VERSION,
272
+ source_session_id: context.sessionId,
273
+ created_at: createdAt,
274
+ cwd: redact(context.cwd),
275
+ git_head: gitHead,
276
+ changed_files: files,
277
+ artifacts,
278
+ redactions: [...redactions.labels].sort(),
279
+ recommended_output: [
280
+ "title",
281
+ "problem summary",
282
+ "reproduction/context",
283
+ "proposed fix or implementation plan",
284
+ "affected files",
285
+ "tests to run",
286
+ "uncertainty / remaining risks",
287
+ ],
288
+ worker_prompt_path: workerPromptPath,
289
+ };
290
+ await Bun.write(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
291
+
292
+ let spawned = false;
293
+ if (options.spawnWorker) {
294
+ const spawn =
295
+ options.spawn ??
296
+ (async (args, cwd, shell) => {
297
+ if (shell) {
298
+ Bun.spawn({
299
+ cmd: args,
300
+ cwd,
301
+ stdout: "inherit",
302
+ stderr: "inherit",
303
+ stdin: "inherit",
304
+ windowsVerbatimArguments: true,
305
+ });
306
+ return;
307
+ }
308
+ Bun.spawn(args, { cwd, stdout: "inherit", stderr: "inherit", stdin: "inherit" });
309
+ });
310
+ const command = resolveGjcCommand();
311
+ await spawn(
312
+ [command.cmd, ...command.args, "--no-skills", "--", `@${workerPromptPath}`],
313
+ context.cwd,
314
+ command.shell,
315
+ );
316
+ spawned = true;
317
+ }
318
+
319
+ return { manifestPath, workerPromptPath, artifactDir, changedFiles: files, spawned };
320
+ }
@@ -176,6 +176,8 @@ export interface SessionInitEntry extends SessionEntryBase {
176
176
  tools: string[];
177
177
  /** Output schema if structured output was requested */
178
178
  outputSchema?: unknown;
179
+ /** Fork-context seed metadata for subagent debugging/replay. */
180
+ forkContext?: unknown;
179
181
  }
180
182
 
181
183
  /** Mode change entry - tracks agent mode transitions (e.g. plan mode). */
@@ -2714,7 +2716,13 @@ export class SessionManager {
2714
2716
  }
2715
2717
 
2716
2718
  /** Append session init metadata (for subagent debugging/replay). Returns entry id. */
2717
- appendSessionInit(init: { systemPrompt: string; task: string; tools: string[]; outputSchema?: unknown }): string {
2719
+ appendSessionInit(init: {
2720
+ systemPrompt: string;
2721
+ task: string;
2722
+ tools: string[];
2723
+ outputSchema?: unknown;
2724
+ forkContext?: unknown;
2725
+ }): string {
2718
2726
  const entry: SessionInitEntry = {
2719
2727
  type: "session_init",
2720
2728
  id: generateId(this.#byId),
@@ -1,5 +1,6 @@
1
1
  export const MODEL_ONBOARDING_API_PROVIDER_COMMAND =
2
2
  "/provider add --compat <openai|anthropic> --provider <id> --base-url <url> --api-key-env <ENV> --model <model>";
3
+ export const MODEL_ONBOARDING_PROVIDER_PRESET_COMMAND = "/provider add --preset <minimax|minimax-cn|glm>";
3
4
 
4
5
  export const MODEL_ONBOARDING_SETUP_COMMAND = "gjc setup provider";
5
6
  export const MODEL_ONBOARDING_OAUTH_COMMAND = "/provider login [provider-id] or /login [provider-id]";
@@ -9,14 +10,15 @@ export function formatModelOnboardingGuidance(): string {
9
10
  "Model selection only shows configured providers.",
10
11
  "Assignment targets are DEFAULT plus the GJC role agents: EXECUTOR, ARCHITECT, PLANNER, and CRITIC.",
11
12
  "Legacy model-role aliases are compatibility-only and are not shown as assignment targets.",
12
- `API-compatible providers: ${MODEL_ONBOARDING_API_PROVIDER_COMMAND} (or ${MODEL_ONBOARDING_SETUP_COMMAND}).`,
13
+ `Provider presets: ${MODEL_ONBOARDING_PROVIDER_PRESET_COMMAND} (or ${MODEL_ONBOARDING_SETUP_COMMAND} --preset <preset>).`,
14
+ `API-compatible custom providers: ${MODEL_ONBOARDING_API_PROVIDER_COMMAND}.`,
13
15
  `OAuth/subscription providers: ${MODEL_ONBOARDING_OAUTH_COMMAND}.`,
14
16
  "Then run /model to select a configured model or assign it to a target.",
15
17
  ].join("\n");
16
18
  }
17
19
 
18
20
  export function formatModelOnboardingInlineHint(): string {
19
- return `Add API-compatible providers with ${MODEL_ONBOARDING_API_PROVIDER_COMMAND} (or ${MODEL_ONBOARDING_SETUP_COMMAND}); OAuth/subscription with ${MODEL_ONBOARDING_OAUTH_COMMAND}; then run /model for DEFAULT, EXECUTOR, ARCHITECT, PLANNER, and CRITIC.`;
21
+ return `Add MiniMax/GLM presets with ${MODEL_ONBOARDING_PROVIDER_PRESET_COMMAND}; custom API providers with ${MODEL_ONBOARDING_API_PROVIDER_COMMAND} (or ${MODEL_ONBOARDING_SETUP_COMMAND}); OAuth/subscription with ${MODEL_ONBOARDING_OAUTH_COMMAND}; then run /model for DEFAULT, EXECUTOR, ARCHITECT, PLANNER, and CRITIC.`;
20
22
  }
21
23
 
22
24
  export function formatNoModelOnboardingError(): string {
@@ -27,7 +29,8 @@ export function formatNoCredentialOnboardingError(providerId: string): string {
27
29
  return [
28
30
  `No credentials found for ${providerId}.`,
29
31
  "",
30
- `For API-compatible providers, configure credentials with ${MODEL_ONBOARDING_API_PROVIDER_COMMAND} (or ${MODEL_ONBOARDING_SETUP_COMMAND}).`,
32
+ `For MiniMax/GLM presets, configure credentials with ${MODEL_ONBOARDING_PROVIDER_PRESET_COMMAND} (or ${MODEL_ONBOARDING_SETUP_COMMAND} --preset <preset>).`,
33
+ `For custom API-compatible providers, use ${MODEL_ONBOARDING_API_PROVIDER_COMMAND}.`,
31
34
  `For OAuth/subscription providers, use ${MODEL_ONBOARDING_OAUTH_COMMAND}.`,
32
35
  "Then run /model to select a configured model or assign it to DEFAULT, EXECUTOR, ARCHITECT, PLANNER, or CRITIC.",
33
36
  ].join("\n");
@@ -5,14 +5,16 @@ import { YAML } from "bun";
5
5
  import { type ModelsConfig, ModelsConfigSchema } from "../config/models-config-schema";
6
6
 
7
7
  export type ProviderCompatibility = "openai" | "anthropic";
8
+ export type ProviderSetupApi = "openai-responses" | "openai-completions" | "anthropic-messages";
8
9
 
9
10
  export interface ProviderSetupInput {
10
- compatibility: ProviderCompatibility;
11
- providerId: string;
12
- baseUrl: string;
11
+ compatibility?: ProviderCompatibility;
12
+ preset?: string;
13
+ providerId?: string;
14
+ baseUrl?: string;
13
15
  apiKey?: string;
14
16
  apiKeyEnv?: string;
15
- models: string[];
17
+ models?: string[];
16
18
  modelsPath?: string;
17
19
  force?: boolean;
18
20
  }
@@ -20,19 +22,93 @@ export interface ProviderSetupInput {
20
22
  export interface ProviderSetupResult {
21
23
  providerId: string;
22
24
  compatibility: ProviderCompatibility;
23
- api: "openai-responses" | "anthropic-messages";
25
+ api: ProviderSetupApi;
24
26
  baseUrl: string;
25
27
  modelIds: string[];
26
28
  modelsPath: string;
27
29
  redactedApiKey: string;
28
30
  credentialSource: "literal" | "env";
31
+ preset?: string;
32
+ presetName?: string;
29
33
  }
30
34
 
31
35
  type ProviderConfig = NonNullable<NonNullable<ModelsConfig["providers"]>[string]>;
36
+ type ProviderCompatConfig = NonNullable<ProviderConfig["compat"]>;
37
+
38
+ interface ProviderPreset {
39
+ id: string;
40
+ aliases: readonly string[];
41
+ name: string;
42
+ description: string;
43
+ compatibility: ProviderCompatibility;
44
+ api: ProviderSetupApi;
45
+ providerId: string;
46
+ baseUrl: string;
47
+ apiKeyEnv: string;
48
+ models: readonly string[];
49
+ compat?: ProviderCompatConfig;
50
+ }
32
51
 
33
52
  const PROVIDER_ID_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
34
53
  const REDACT_PREFIX = 4;
35
54
  const REDACT_SUFFIX = 4;
55
+ // Preset compat values are onboarding snapshots for generated models.yml entries.
56
+ // Keep them aligned with provider descriptor behavior without importing descriptor internals into setup UX.
57
+ const MINIMAX_OPENAI_COMPAT: ProviderCompatConfig = {
58
+ supportsStore: false,
59
+ supportsDeveloperRole: false,
60
+ supportsReasoningEffort: false,
61
+ reasoningContentField: "reasoning_content",
62
+ };
63
+
64
+ const GLM_OPENAI_COMPAT: ProviderCompatConfig = {
65
+ supportsDeveloperRole: false,
66
+ supportsReasoningEffort: false,
67
+ thinkingFormat: "zai",
68
+ reasoningContentField: "reasoning_content",
69
+ };
70
+
71
+ export const PROVIDER_PRESETS: readonly ProviderPreset[] = [
72
+ {
73
+ id: "minimax",
74
+ aliases: ["minimax-code"],
75
+ name: "MiniMax Coding Plan",
76
+ description: "OpenAI-compatible MiniMax Coding Plan endpoint",
77
+ compatibility: "openai",
78
+ api: "openai-completions",
79
+ providerId: "minimax-code",
80
+ baseUrl: "https://api.minimax.io/v1",
81
+ apiKeyEnv: "MINIMAX_CODE_API_KEY",
82
+ models: ["MiniMax-M2.5"],
83
+ compat: MINIMAX_OPENAI_COMPAT,
84
+ },
85
+ {
86
+ id: "minimax-cn",
87
+ aliases: ["minimax-code-cn", "minimaxi"],
88
+ name: "MiniMax Coding Plan (China)",
89
+ description: "OpenAI-compatible MiniMax China endpoint",
90
+ compatibility: "openai",
91
+ api: "openai-completions",
92
+ providerId: "minimax-code-cn",
93
+ baseUrl: "https://api.minimaxi.com/v1",
94
+ apiKeyEnv: "MINIMAX_CODE_CN_API_KEY",
95
+ models: ["MiniMax-M2.5"],
96
+ compat: MINIMAX_OPENAI_COMPAT,
97
+ },
98
+ {
99
+ id: "glm",
100
+ aliases: ["zai", "z-ai", "bigmodel"],
101
+ name: "GLM / zAI",
102
+ description: "OpenAI-compatible GLM endpoint from zAI/BigModel",
103
+ compatibility: "openai",
104
+ api: "openai-completions",
105
+ providerId: "glm-proxy",
106
+ baseUrl: "https://api.z.ai/api/paas/v4",
107
+ apiKeyEnv: "ZAI_API_KEY",
108
+ models: ["glm-4.6"],
109
+ compat: GLM_OPENAI_COMPAT,
110
+ },
111
+ ];
36
112
 
37
113
  export function getDefaultModelsPath(): string {
38
114
  return path.join(getAgentDir(), "models.yml");
@@ -51,6 +127,19 @@ export function parseProviderCompatibility(value: string): ProviderCompatibility
51
127
  throw new Error("Provider compatibility must be 'openai' or 'anthropic'.");
52
128
  }
53
129
 
130
+ export function findProviderPreset(value: string | undefined): ProviderPreset | undefined {
131
+ const normalized = value?.trim().toLowerCase();
132
+ if (!normalized) return undefined;
133
+ return PROVIDER_PRESETS.find(preset => preset.id === normalized || preset.aliases.includes(normalized));
134
+ }
135
+
136
+ export function formatProviderPresetList(): string {
137
+ return PROVIDER_PRESETS.map(preset => {
138
+ const aliases = preset.aliases.length > 0 ? ` (aliases: ${preset.aliases.join(", ")})` : "";
139
+ return `${preset.id}${aliases}: ${preset.description}`;
140
+ }).join("\n");
141
+ }
142
+
54
143
  export function parseModelList(values: readonly string[]): string[] {
55
144
  const models = values
56
145
  .flatMap(value => value.split(","))
@@ -65,23 +154,82 @@ export function redactSecret(secret: string): string {
65
154
  return `${trimmed.slice(0, REDACT_PREFIX)}…${trimmed.slice(-REDACT_SUFFIX)}`;
66
155
  }
67
156
 
68
- function apiForCompatibility(compatibility: ProviderCompatibility): ProviderSetupResult["api"] {
157
+ function apiForCompatibility(compatibility: ProviderCompatibility): ProviderSetupApi {
69
158
  return compatibility === "openai" ? "openai-responses" : "anthropic-messages";
70
159
  }
71
160
 
161
+ function resolvePresetInput(input: ProviderSetupInput): {
162
+ compatibility: ProviderCompatibility;
163
+ preset?: ProviderPreset;
164
+ providerId?: string;
165
+ baseUrl?: string;
166
+ apiKey?: string;
167
+ apiKeyEnv?: string;
168
+ models: readonly string[];
169
+ api: ProviderSetupApi;
170
+ compat?: ProviderCompatConfig;
171
+ } {
172
+ const preset = input.preset ? findProviderPreset(input.preset) : undefined;
173
+ if (input.preset && !preset) {
174
+ throw new Error(`Unknown provider preset '${input.preset}'. Available presets:\n${formatProviderPresetList()}`);
175
+ }
176
+ if (preset && input.compatibility && input.compatibility !== preset.compatibility) {
177
+ throw new Error(
178
+ `Provider preset '${preset.id}' is ${preset.compatibility}-compatible; omit --compat or use '${preset.compatibility}'.`,
179
+ );
180
+ }
181
+ if (preset && input.baseUrl !== undefined) {
182
+ throw new Error(
183
+ `Provider preset '${preset.id}' uses a fixed base URL; omit --base-url or use --compat openai for a custom provider.`,
184
+ );
185
+ }
186
+ if (preset && input.models && input.models.length > 0) {
187
+ throw new Error(
188
+ `Provider preset '${preset.id}' uses fixed model ids; omit --model or use --compat openai for a custom provider.`,
189
+ );
190
+ }
191
+ if (preset && input.apiKeyEnv !== undefined && input.apiKeyEnv.trim() !== preset.apiKeyEnv) {
192
+ throw new Error(
193
+ `Provider preset '${preset.id}' uses ${preset.apiKeyEnv}; omit --api-key-env or use --compat openai for a custom provider.`,
194
+ );
195
+ }
196
+ const compatibility = preset?.compatibility ?? input.compatibility;
197
+ if (!compatibility) {
198
+ throw new Error("Provider compatibility is required unless --preset is used.");
199
+ }
200
+ return {
201
+ compatibility,
202
+ preset,
203
+ providerId: input.providerId ?? preset?.providerId,
204
+ baseUrl: input.baseUrl ?? preset?.baseUrl,
205
+ apiKey: input.apiKey,
206
+ apiKeyEnv: input.apiKeyEnv ?? preset?.apiKeyEnv,
207
+ models: input.models && input.models.length > 0 ? input.models : (preset?.models ?? []),
208
+ api: preset?.api ?? apiForCompatibility(compatibility),
209
+ compat: preset?.compat,
210
+ };
211
+ }
212
+
72
213
  function validateSetupInput(input: ProviderSetupInput): {
73
214
  providerId: string;
74
215
  baseUrl: string;
75
216
  apiKey: string;
76
217
  credentialSource: ProviderSetupResult["credentialSource"];
77
218
  models: string[];
219
+ compatibility: ProviderCompatibility;
220
+ api: ProviderSetupApi;
221
+ compat?: ProviderCompatConfig;
222
+ preset?: ProviderPreset;
78
223
  } {
79
- const providerId = normalizeProviderId(input.providerId);
224
+ const resolved = resolvePresetInput(input);
225
+ if (!resolved.providerId) throw new Error("Provider id is required.");
226
+ if (!resolved.baseUrl) throw new Error("Base URL is required.");
227
+ const providerId = normalizeProviderId(resolved.providerId);
80
228
  if (!PROVIDER_ID_PATTERN.test(providerId)) {
81
229
  throw new Error("Provider id must use lowercase letters, numbers, dots, underscores, or hyphens.");
82
230
  }
83
231
 
84
- const baseUrl = input.baseUrl.trim();
232
+ const baseUrl = resolved.baseUrl.trim();
85
233
  let url: URL;
86
234
  try {
87
235
  url = new URL(baseUrl);
@@ -95,19 +243,29 @@ function validateSetupInput(input: ProviderSetupInput): {
95
243
  throw new Error("Base URL must use https unless it targets localhost or a loopback address.");
96
244
  }
97
245
 
98
- const apiKeyEnv = input.apiKeyEnv?.trim();
246
+ const apiKeyEnv = resolved.apiKeyEnv?.trim();
99
247
  if (apiKeyEnv) {
100
248
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(apiKeyEnv)) {
101
249
  throw new Error("API key environment variable must be a valid environment variable name.");
102
250
  }
103
251
  }
104
- const apiKey = apiKeyEnv ?? input.apiKey?.trim() ?? "";
252
+ const apiKey = apiKeyEnv ?? resolved.apiKey?.trim() ?? "";
105
253
  if (!apiKey) throw new Error("API key is required.");
106
254
 
107
- const models = parseModelList(input.models);
255
+ const models = parseModelList(resolved.models);
108
256
  if (models.length === 0) throw new Error("At least one model id is required.");
109
257
 
110
- return { providerId, baseUrl, apiKey, credentialSource: apiKeyEnv ? "env" : "literal", models };
258
+ return {
259
+ providerId,
260
+ baseUrl,
261
+ apiKey,
262
+ credentialSource: apiKeyEnv ? "env" : "literal",
263
+ models,
264
+ compatibility: resolved.compatibility,
265
+ api: resolved.api,
266
+ compat: resolved.compat,
267
+ preset: resolved.preset,
268
+ };
111
269
  }
112
270
 
113
271
  async function readModelsConfig(modelsPath: string): Promise<ModelsConfig> {
@@ -140,16 +298,16 @@ export async function addApiCompatibleProvider(input: ProviderSetupInput): Promi
140
298
  const validated = validateSetupInput(input);
141
299
  const modelsPath = input.modelsPath ?? getDefaultModelsPath();
142
300
  const existing = await readModelsConfig(modelsPath);
143
- const api = apiForCompatibility(input.compatibility);
144
301
  if (existing.providers?.[validated.providerId] && !input.force) {
145
302
  throw new Error(`Provider '${validated.providerId}' already exists. Use --force to replace it.`);
146
303
  }
147
304
  const provider: ProviderConfig = {
148
305
  baseUrl: validated.baseUrl,
149
- api,
306
+ api: validated.api,
150
307
  auth: "apiKey",
151
308
  models: validated.models.map(id => ({ id })),
152
309
  };
310
+ if (validated.compat) provider.compat = validated.compat;
153
311
  if (validated.credentialSource === "env") {
154
312
  provider.apiKeyEnv = validated.apiKey;
155
313
  } else {
@@ -165,13 +323,15 @@ export async function addApiCompatibleProvider(input: ProviderSetupInput): Promi
165
323
  await writeModelsConfig(modelsPath, next);
166
324
  return {
167
325
  providerId: validated.providerId,
168
- compatibility: input.compatibility,
169
- api,
326
+ compatibility: validated.compatibility,
327
+ api: validated.api,
170
328
  baseUrl: validated.baseUrl,
171
329
  modelIds: validated.models,
172
330
  modelsPath,
173
331
  redactedApiKey: redactSecret(validated.apiKey),
174
332
  credentialSource: validated.credentialSource,
333
+ preset: validated.preset?.id,
334
+ presetName: validated.preset?.name,
175
335
  };
176
336
  }
177
337
 
@@ -189,6 +349,7 @@ function isLocalHttpHost(hostname: string): boolean {
189
349
  export function formatProviderSetupResult(result: ProviderSetupResult): string {
190
350
  return [
191
351
  `Provider '${result.providerId}' configured as ${result.compatibility}-compatible.`,
352
+ ...(result.presetName ? [`Preset: ${result.presetName}`] : []),
192
353
  `Models: ${result.modelIds.join(", ")}`,
193
354
  `Base URL: ${result.baseUrl}`,
194
355
  `API key: ${result.credentialSource === "env" ? `${result.redactedApiKey} (environment variable)` : result.redactedApiKey}`,