@gajae-code/coding-agent 0.8.1 → 0.8.2
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.
- package/CHANGELOG.md +34 -0
- package/dist/types/cli/completion-cli.d.ts +41 -0
- package/dist/types/cli/config-cli.d.ts +1 -0
- package/dist/types/cli/notify-cli.d.ts +7 -0
- package/dist/types/cli.d.ts +124 -1
- package/dist/types/commands/completion.d.ts +29 -0
- package/dist/types/commands/config.d.ts +3 -0
- package/dist/types/commands/deep-interview.d.ts +3 -0
- package/dist/types/config/file-lock.d.ts +7 -6
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/config/model-profile-activation.d.ts +6 -0
- package/dist/types/config/settings.d.ts +8 -0
- package/dist/types/extensibility/extensions/runner.d.ts +2 -1
- package/dist/types/extensibility/extensions/types.d.ts +8 -0
- package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +39 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +10 -1
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -1
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +12 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +2 -1
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
- package/dist/types/main.d.ts +4 -1
- package/dist/types/modes/components/model-selector.d.ts +4 -0
- package/dist/types/modes/components/queued-message-selector.d.ts +12 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/controllers/input-controller.d.ts +1 -3
- package/dist/types/modes/print-mode.d.ts +13 -1
- package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -1
- package/dist/types/notifications/config.d.ts +8 -1
- package/dist/types/notifications/daemon-paths.d.ts +1 -0
- package/dist/types/notifications/index.d.ts +6 -3
- package/dist/types/notifications/lifecycle-commands.d.ts +11 -3
- package/dist/types/notifications/telegram-daemon.d.ts +15 -3
- package/dist/types/notifications/telegram-reference.d.ts +0 -5
- package/dist/types/runtime-api-key.d.ts +7 -0
- package/dist/types/session/agent-session.d.ts +10 -0
- package/dist/types/session/agent-storage.d.ts +1 -0
- package/dist/types/task/executor.d.ts +2 -0
- package/dist/types/tools/computer.d.ts +2 -2
- package/dist/types/workflow/workflow-intent-diff.d.ts +8 -2
- package/dist/types/workflow/workflow-intent-report-consumer.d.ts +50 -0
- package/dist/types/workflow/workflow-intent-report-schema.d.ts +2 -0
- package/dist/types/workflow/workflow-intent-report.d.ts +48 -0
- package/package.json +7 -7
- package/src/cli/completion-cli.ts +234 -0
- package/src/cli/config-cli.ts +37 -9
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/notify-cli.ts +91 -7
- package/src/cli.ts +3 -1
- package/src/commands/completion.ts +84 -0
- package/src/commands/config.ts +2 -0
- package/src/commands/deep-interview.ts +2 -1
- package/src/config/file-lock.ts +73 -34
- package/src/config/keybindings.ts +2 -2
- package/src/config/model-profile-activation.ts +65 -0
- package/src/config/model-registry.ts +14 -2
- package/src/config/settings.ts +22 -0
- package/src/coordinator-mcp/server.ts +34 -8
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +38 -5
- package/src/defaults/gjc/skills/ralplan/SKILL.md +15 -11
- package/src/defaults/gjc/skills/team/SKILL.md +1 -1
- package/src/extensibility/extensions/runner.ts +2 -0
- package/src/extensibility/extensions/types.ts +9 -0
- package/src/gjc-runtime/deep-interview-ambiguity.ts +206 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +62 -8
- package/src/gjc-runtime/deep-interview-runtime.ts +210 -2
- package/src/gjc-runtime/deep-interview-state.ts +13 -0
- package/src/gjc-runtime/session-state-sidecar.ts +59 -11
- package/src/gjc-runtime/state-runtime.ts +12 -2
- package/src/gjc-runtime/team-runtime.ts +23 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +73 -19
- package/src/hooks/native-skill-hook.ts +29 -0
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/main.ts +52 -41
- package/src/modes/components/model-selector.ts +54 -11
- package/src/modes/components/queued-message-selector.ts +92 -0
- package/src/modes/components/settings-selector.ts +4 -0
- package/src/modes/components/welcome.ts +160 -44
- package/src/modes/controllers/command-controller.ts +1 -2
- package/src/modes/controllers/extension-ui-controller.ts +4 -10
- package/src/modes/controllers/input-controller.ts +179 -17
- package/src/modes/controllers/selector-controller.ts +122 -27
- package/src/modes/interactive-mode.ts +43 -16
- package/src/modes/print-mode.ts +43 -4
- package/src/modes/prompt-action-autocomplete.ts +21 -20
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +2 -2
- package/src/notifications/config.ts +9 -1
- package/src/notifications/daemon-paths.ts +2 -0
- package/src/notifications/index.ts +39 -9
- package/src/notifications/lifecycle-commands.ts +37 -7
- package/src/notifications/lifecycle-orchestrator.ts +19 -6
- package/src/notifications/telegram-daemon-control.ts +3 -1
- package/src/notifications/telegram-daemon.ts +217 -43
- package/src/notifications/telegram-reference.ts +0 -16
- package/src/prompts/system/system-prompt.md +3 -0
- package/src/prompts/tools/computer.md +2 -2
- package/src/runtime-api-key.ts +16 -0
- package/src/sdk.ts +20 -6
- package/src/session/agent-session.ts +225 -31
- package/src/session/agent-storage.ts +4 -0
- package/src/session/session-manager.ts +44 -2
- package/src/slash-commands/builtin-registry.ts +153 -71
- package/src/task/executor.ts +7 -4
- package/src/task/index.ts +9 -3
- package/src/task/output-manager.ts +16 -6
- package/src/task/receipt.ts +26 -7
- package/src/tools/computer.ts +246 -43
- package/src/tools/telegram-send.ts +6 -0
- package/src/workflow/workflow-intent-diff.ts +29 -6
- package/src/workflow/workflow-intent-report-consumer.ts +221 -0
- package/src/workflow/workflow-intent-report-schema.ts +129 -0
- package/src/workflow/workflow-intent-report.ts +135 -0
package/src/cli/config-cli.ts
CHANGED
|
@@ -33,6 +33,7 @@ export interface ConfigCommandArgs {
|
|
|
33
33
|
value?: string;
|
|
34
34
|
flags: {
|
|
35
35
|
json?: boolean;
|
|
36
|
+
showSecrets?: boolean;
|
|
36
37
|
};
|
|
37
38
|
}
|
|
38
39
|
// =============================================================================
|
|
@@ -47,6 +48,19 @@ type CliSettingDef = {
|
|
|
47
48
|
};
|
|
48
49
|
|
|
49
50
|
const ALL_SETTING_PATHS = Object.keys(SETTINGS_SCHEMA) as SettingPath[];
|
|
51
|
+
const REDACTED_SECRET_VALUE = "<redacted>";
|
|
52
|
+
const SECRET_SETTING_SEGMENT_PATTERN = /(?:api[-_]?key|token|secret|password|passwd|pwd|credential)/i;
|
|
53
|
+
|
|
54
|
+
function isSecretSettingPath(path: string): boolean {
|
|
55
|
+
return path.split(".").some(segment => SECRET_SETTING_SEGMENT_PATTERN.test(segment));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function redactConfigValue(path: string, value: unknown, showSecrets?: boolean): unknown {
|
|
59
|
+
if (showSecrets || typeof value !== "string" || !isSecretSettingPath(path)) {
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
return REDACTED_SECRET_VALUE;
|
|
63
|
+
}
|
|
50
64
|
|
|
51
65
|
/** Find setting definition by path */
|
|
52
66
|
function findSettingDef(path: string): CliSettingDef | undefined {
|
|
@@ -105,6 +119,8 @@ export function parseConfigArgs(args: string[]): ConfigCommandArgs | undefined {
|
|
|
105
119
|
const arg = args[i];
|
|
106
120
|
if (arg === "--json") {
|
|
107
121
|
result.flags.json = true;
|
|
122
|
+
} else if (arg === "--show-secrets") {
|
|
123
|
+
result.flags.showSecrets = true;
|
|
108
124
|
} else if (!arg.startsWith("-")) {
|
|
109
125
|
positionalArgs.push(arg);
|
|
110
126
|
}
|
|
@@ -265,14 +281,15 @@ export async function runConfigCommand(cmd: ConfigCommandArgs): Promise<void> {
|
|
|
265
281
|
}
|
|
266
282
|
}
|
|
267
283
|
|
|
268
|
-
function handleList(flags: { json?: boolean }): void {
|
|
284
|
+
function handleList(flags: { json?: boolean; showSecrets?: boolean }): void {
|
|
269
285
|
const defs = ALL_SETTING_PATHS.map(path => findSettingDef(path)).filter((def): def is CliSettingDef => !!def);
|
|
270
286
|
|
|
271
287
|
if (flags.json) {
|
|
272
288
|
const result: Record<string, { value: unknown; type: string; description: string }> = {};
|
|
273
289
|
for (const def of defs) {
|
|
290
|
+
const value = settings.get(def.path);
|
|
274
291
|
result[def.path] = {
|
|
275
|
-
value:
|
|
292
|
+
value: redactConfigValue(def.path, value, flags.showSecrets),
|
|
276
293
|
type: def.type,
|
|
277
294
|
description: def.description,
|
|
278
295
|
};
|
|
@@ -301,7 +318,8 @@ function handleList(flags: { json?: boolean }): void {
|
|
|
301
318
|
console.log(chalk.bold.blue(`[${group}]`));
|
|
302
319
|
for (const def of groups[group]) {
|
|
303
320
|
const value = settings.get(def.path);
|
|
304
|
-
const
|
|
321
|
+
const displayValue = redactConfigValue(def.path, value, flags.showSecrets);
|
|
322
|
+
const valueStr = formatValue(displayValue);
|
|
305
323
|
const typeStr = getTypeDisplay(def);
|
|
306
324
|
console.log(` ${chalk.white(def.path)} = ${valueStr} ${chalk.dim(typeStr)}`);
|
|
307
325
|
}
|
|
@@ -309,7 +327,7 @@ function handleList(flags: { json?: boolean }): void {
|
|
|
309
327
|
}
|
|
310
328
|
}
|
|
311
329
|
|
|
312
|
-
function handleGet(key: string | undefined, flags: { json?: boolean }): void {
|
|
330
|
+
function handleGet(key: string | undefined, flags: { json?: boolean; showSecrets?: boolean }): void {
|
|
313
331
|
if (!key) {
|
|
314
332
|
console.error(chalk.red(`Usage: ${APP_NAME} config get <key>`));
|
|
315
333
|
console.error(chalk.dim(`\nRun '${APP_NAME} config list' to see available keys`));
|
|
@@ -324,16 +342,23 @@ function handleGet(key: string | undefined, flags: { json?: boolean }): void {
|
|
|
324
342
|
}
|
|
325
343
|
|
|
326
344
|
const value = settings.get(def.path);
|
|
345
|
+
const displayValue = redactConfigValue(def.path, value, flags.showSecrets);
|
|
327
346
|
|
|
328
347
|
if (flags.json) {
|
|
329
|
-
console.log(
|
|
348
|
+
console.log(
|
|
349
|
+
JSON.stringify({ key: def.path, value: displayValue, type: def.type, description: def.description }, null, 2),
|
|
350
|
+
);
|
|
330
351
|
return;
|
|
331
352
|
}
|
|
332
353
|
|
|
333
|
-
console.log(formatValue(
|
|
354
|
+
console.log(formatValue(displayValue));
|
|
334
355
|
}
|
|
335
356
|
|
|
336
|
-
async function handleSet(
|
|
357
|
+
async function handleSet(
|
|
358
|
+
key: string | undefined,
|
|
359
|
+
value: string | undefined,
|
|
360
|
+
flags: { json?: boolean; showSecrets?: boolean },
|
|
361
|
+
): Promise<void> {
|
|
337
362
|
if (!key || value === undefined) {
|
|
338
363
|
console.error(chalk.red(`Usage: ${APP_NAME} config set <key> <value>`));
|
|
339
364
|
console.error(chalk.dim(`\nRun '${APP_NAME} config list' to see available keys`));
|
|
@@ -355,11 +380,12 @@ async function handleSet(key: string | undefined, value: string | undefined, fla
|
|
|
355
380
|
}
|
|
356
381
|
|
|
357
382
|
const newValue = settings.get(def.path);
|
|
383
|
+
const displayValue = redactConfigValue(def.path, newValue, flags.showSecrets);
|
|
358
384
|
|
|
359
385
|
if (flags.json) {
|
|
360
|
-
console.log(JSON.stringify({ key: def.path, value:
|
|
386
|
+
console.log(JSON.stringify({ key: def.path, value: displayValue }));
|
|
361
387
|
} else {
|
|
362
|
-
console.log(chalk.green(`${theme.status.success} Set ${def.path} = ${formatValue(
|
|
388
|
+
console.log(chalk.green(`${theme.status.success} Set ${def.path} = ${formatValue(displayValue)}`));
|
|
363
389
|
}
|
|
364
390
|
}
|
|
365
391
|
|
|
@@ -409,6 +435,7 @@ ${chalk.bold("Commands:")}
|
|
|
409
435
|
|
|
410
436
|
${chalk.bold("Options:")}
|
|
411
437
|
--json Output as JSON
|
|
438
|
+
--show-secrets Show secret-like setting values without redaction (unsafe)
|
|
412
439
|
|
|
413
440
|
${chalk.bold("Examples:")}
|
|
414
441
|
${APP_NAME} config list
|
|
@@ -418,6 +445,7 @@ ${chalk.bold("Examples:")}
|
|
|
418
445
|
${APP_NAME} config set defaultThinkingLevel medium
|
|
419
446
|
${APP_NAME} config reset steeringMode
|
|
420
447
|
${APP_NAME} config list --json
|
|
448
|
+
${APP_NAME} config get auth.broker.token --show-secrets
|
|
421
449
|
${APP_NAME} config init-xdg
|
|
422
450
|
|
|
423
451
|
${chalk.bold("Boolean Values:")}
|
package/src/cli/fast-help.ts
CHANGED
|
@@ -15,6 +15,7 @@ export function getExtraHelpText(): string {
|
|
|
15
15
|
${APP_NAME} deep-interview - Run requirements interview workflow
|
|
16
16
|
${APP_NAME} skills - List/read embedded workflow skills
|
|
17
17
|
${APP_NAME} config - List, get, and set configuration values
|
|
18
|
+
${APP_NAME} stats - View AI usage statistics (tokens, cost, requests)
|
|
18
19
|
${APP_NAME} notify - Send or test notifications
|
|
19
20
|
${APP_NAME} daemon - Manage background daemon helpers
|
|
20
21
|
${APP_NAME} mcp - Manage MCP server registrations
|
package/src/cli/notify-cli.ts
CHANGED
|
@@ -58,6 +58,11 @@ interface TelegramUser {
|
|
|
58
58
|
allows_users_to_create_topics?: boolean;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
interface TelegramChat {
|
|
62
|
+
id?: number | string;
|
|
63
|
+
type?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
61
66
|
type ThreadedModeState = "enabled" | "disabled" | "unknown";
|
|
62
67
|
type ThreadedModeFinalLabel = "verified" | "unverified" | "unknown";
|
|
63
68
|
|
|
@@ -148,6 +153,7 @@ async function runSetup(deps: NotifyCommandDeps): Promise<void> {
|
|
|
148
153
|
let chatId: string;
|
|
149
154
|
if (deps.setupChatId?.trim()) {
|
|
150
155
|
chatId = deps.setupChatId.trim();
|
|
156
|
+
await verifyPrivateChatId(fetchImpl, apiBase, token, chatId);
|
|
151
157
|
process.stdout.write(`Using provided chat id ${chatId} (non-interactive).\n`);
|
|
152
158
|
} else {
|
|
153
159
|
const stale = await getUpdates(fetchImpl, apiBase, token, { timeout: 0, allowed_updates: ["message"] });
|
|
@@ -170,16 +176,76 @@ async function runSetup(deps: NotifyCommandDeps): Promise<void> {
|
|
|
170
176
|
);
|
|
171
177
|
}
|
|
172
178
|
|
|
173
|
-
|
|
174
|
-
|
|
179
|
+
type TokenPromptInput = NodeJS.ReadStream & {
|
|
180
|
+
isRaw?: boolean;
|
|
181
|
+
setRawMode?: (mode: boolean) => unknown;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
type TokenPromptOutput = Pick<NodeJS.WriteStream, "write">;
|
|
185
|
+
|
|
186
|
+
export async function promptForToken(
|
|
187
|
+
input: TokenPromptInput = process.stdin,
|
|
188
|
+
output: TokenPromptOutput = process.stdout,
|
|
189
|
+
): Promise<string> {
|
|
190
|
+
if (!input.isTTY) {
|
|
175
191
|
throw new Error("notify setup requires an interactive TTY unless setupToken is injected.");
|
|
176
192
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
return (await rl.question("Telegram BotFather token: ")).trim();
|
|
180
|
-
} finally {
|
|
181
|
-
rl.close();
|
|
193
|
+
if (typeof input.setRawMode !== "function") {
|
|
194
|
+
throw new Error("notify setup requires a TTY with raw input support unless setupToken is injected.");
|
|
182
195
|
}
|
|
196
|
+
|
|
197
|
+
output.write("Telegram BotFather token: ");
|
|
198
|
+
const wasRaw = input.isRaw === true;
|
|
199
|
+
input.setRawMode(true);
|
|
200
|
+
|
|
201
|
+
return await new Promise<string>((resolve, reject) => {
|
|
202
|
+
let value = "";
|
|
203
|
+
let settled = false;
|
|
204
|
+
|
|
205
|
+
const cleanup = () => {
|
|
206
|
+
input.off("data", onData);
|
|
207
|
+
input.off("error", onError);
|
|
208
|
+
input.setRawMode?.(wasRaw);
|
|
209
|
+
output.write("\n");
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const finish = (callback: () => void) => {
|
|
213
|
+
if (settled) return;
|
|
214
|
+
settled = true;
|
|
215
|
+
cleanup();
|
|
216
|
+
callback();
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const accept = () => finish(() => resolve(value.trim()));
|
|
220
|
+
const cancel = () => finish(() => reject(new Error("Telegram bot token prompt cancelled.")));
|
|
221
|
+
const onError = (error: Error) => finish(() => reject(error));
|
|
222
|
+
const onData = (chunk: Buffer | string) => {
|
|
223
|
+
for (const char of String(chunk)) {
|
|
224
|
+
if (char === "\r" || char === "\n") {
|
|
225
|
+
accept();
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (char === "\u0003") {
|
|
229
|
+
cancel();
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (char === "\u0004") {
|
|
233
|
+
if (value) accept();
|
|
234
|
+
else cancel();
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (char === "\u007f" || char === "\b") {
|
|
238
|
+
value = value.slice(0, -1);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (char >= " ") value += char;
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
input.on("data", onData);
|
|
246
|
+
input.once("error", onError);
|
|
247
|
+
input.resume();
|
|
248
|
+
});
|
|
183
249
|
}
|
|
184
250
|
|
|
185
251
|
const THREADED_ENABLED_SUCCESS =
|
|
@@ -223,6 +289,24 @@ async function getMe(fetchImpl: typeof fetch, apiBase: string, token: string): P
|
|
|
223
289
|
return user;
|
|
224
290
|
}
|
|
225
291
|
|
|
292
|
+
async function verifyPrivateChatId(
|
|
293
|
+
fetchImpl: typeof fetch,
|
|
294
|
+
apiBase: string,
|
|
295
|
+
token: string,
|
|
296
|
+
chatId: string,
|
|
297
|
+
): Promise<void> {
|
|
298
|
+
const chat = (await callTelegram<unknown>(fetchImpl, apiBase, token, "getChat", { chat_id: chatId })) as
|
|
299
|
+
| TelegramChat
|
|
300
|
+
| undefined;
|
|
301
|
+
if (!chat || typeof chat !== "object") {
|
|
302
|
+
throw new Error("Telegram getChat returned invalid Telegram response: missing valid Chat result.");
|
|
303
|
+
}
|
|
304
|
+
if (chat.type !== "private") {
|
|
305
|
+
const type = typeof chat.type === "string" && chat.type ? chat.type : "unknown";
|
|
306
|
+
throw new Error(`Provided chat id ${chatId} is a ${type} chat; pairing requires a private Telegram chat.`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
226
310
|
function threadedModeState(user: TelegramUser): ThreadedModeState {
|
|
227
311
|
if (user.has_topics_enabled === true) return "enabled";
|
|
228
312
|
if (user.has_topics_enabled === false) return "disabled";
|
package/src/cli.ts
CHANGED
|
@@ -36,6 +36,7 @@ export const commands: CommandEntry[] = [
|
|
|
36
36
|
{ name: "gc", load: () => import("./commands/gc").then(m => m.default) },
|
|
37
37
|
{ name: "ralplan", load: () => import("./commands/ralplan").then(m => m.default) },
|
|
38
38
|
{ name: "config", load: () => import("./commands/config").then(m => m.default) },
|
|
39
|
+
{ name: "stats", load: () => import("./commands/stats").then(m => m.default) },
|
|
39
40
|
{ name: "notify", load: () => import("./commands/notify").then(m => m.default) },
|
|
40
41
|
{ name: "daemon", load: () => import("./commands/daemon").then(m => m.default) },
|
|
41
42
|
{ name: "web-search", aliases: ["q"], load: () => import("./commands/web-search").then(m => m.default) },
|
|
@@ -52,6 +53,7 @@ export const commands: CommandEntry[] = [
|
|
|
52
53
|
{ name: "rlm", load: () => import("./commands/rlm").then(m => m.default) },
|
|
53
54
|
{ name: "update", load: () => import("./commands/update").then(m => m.default) },
|
|
54
55
|
{ name: "plugin", load: () => import("./commands/plugin").then(m => m.default) },
|
|
56
|
+
{ name: "completion", load: () => import("./commands/completion").then(m => m.default) },
|
|
55
57
|
{ name: "launch", load: () => import("./commands/launch").then(m => m.default) },
|
|
56
58
|
];
|
|
57
59
|
|
|
@@ -110,7 +112,7 @@ function hasRootVersionFlag(argv: string[]): boolean {
|
|
|
110
112
|
return hasRootFastFlag(argv, versionFlags);
|
|
111
113
|
}
|
|
112
114
|
|
|
113
|
-
class RootHelpCommand extends Command {
|
|
115
|
+
export class RootHelpCommand extends Command {
|
|
114
116
|
static description = "Red-claw AI coding assistant";
|
|
115
117
|
static hidden = true;
|
|
116
118
|
static args = {
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate shell completion specs for external completion engines.
|
|
3
|
+
*/
|
|
4
|
+
import { Args, Command, Flags } from "@gajae-code/utils/cli";
|
|
5
|
+
import { commands, RootHelpCommand } from "../cli";
|
|
6
|
+
import {
|
|
7
|
+
buildGjcFigSpec,
|
|
8
|
+
defaultInshellisenseSpecDir,
|
|
9
|
+
type InstallGjcInshellisenseSpecResult,
|
|
10
|
+
installGjcInshellisenseSpec,
|
|
11
|
+
renderFigSpecModule,
|
|
12
|
+
} from "../cli/completion-cli";
|
|
13
|
+
|
|
14
|
+
const TARGETS = ["inshellisense", "fig"] as const;
|
|
15
|
+
|
|
16
|
+
type CompletionTarget = (typeof TARGETS)[number];
|
|
17
|
+
|
|
18
|
+
function normalizeTarget(target: CompletionTarget): "inshellisense" {
|
|
19
|
+
return target === "fig" ? "inshellisense" : target;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function formatInstallResult(result: InstallGjcInshellisenseSpecResult, customDir: boolean): string {
|
|
23
|
+
const lines = [
|
|
24
|
+
"Installed GJC inshellisense completion spec:",
|
|
25
|
+
` spec: ${result.specPath}`,
|
|
26
|
+
` index: ${result.indexPath} (${result.indexStatus})`,
|
|
27
|
+
];
|
|
28
|
+
if (customDir) {
|
|
29
|
+
lines.push(
|
|
30
|
+
"Add the install directory to inshellisense's [specs].path setting, then restart the inshellisense session.",
|
|
31
|
+
);
|
|
32
|
+
} else {
|
|
33
|
+
lines.push("Restart the inshellisense session, or run `is reinit` if your shell integration needs a refresh.");
|
|
34
|
+
}
|
|
35
|
+
return `${lines.join("\n")}\n`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default class Completion extends Command {
|
|
39
|
+
static description = "Generate shell completion specs";
|
|
40
|
+
|
|
41
|
+
static args = {
|
|
42
|
+
target: Args.string({
|
|
43
|
+
description: "Completion format",
|
|
44
|
+
required: false,
|
|
45
|
+
options: TARGETS,
|
|
46
|
+
}),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
static flags = {
|
|
50
|
+
install: Flags.boolean({ description: "Install the generated spec for inshellisense" }),
|
|
51
|
+
dir: Flags.string({ description: "Install directory (defaults to ~/.fig/autocomplete/build)" }),
|
|
52
|
+
force: Flags.boolean({ description: "Overwrite an existing non-GJC inshellisense index.js" }),
|
|
53
|
+
json: Flags.boolean({ description: "Output JSON instead of JavaScript or status text" }),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
async run(): Promise<void> {
|
|
57
|
+
const { args, flags } = await this.parse(Completion);
|
|
58
|
+
const target = normalizeTarget((args.target ?? "inshellisense") as CompletionTarget);
|
|
59
|
+
if (target !== "inshellisense") {
|
|
60
|
+
process.stderr.write(`Unsupported completion target: ${target}\n`);
|
|
61
|
+
process.exitCode = 1;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if ((flags.dir || flags.force) && !flags.install) {
|
|
65
|
+
process.stderr.write("--dir and --force only apply with --install\n");
|
|
66
|
+
process.exitCode = 1;
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const spec = await buildGjcFigSpec(commands, RootHelpCommand);
|
|
71
|
+
if (!flags.install) {
|
|
72
|
+
process.stdout.write(flags.json ? `${JSON.stringify(spec, null, 2)}\n` : renderFigSpecModule(spec));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const directory = flags.dir ?? defaultInshellisenseSpecDir();
|
|
77
|
+
const result = await installGjcInshellisenseSpec(spec, { dir: directory, force: flags.force });
|
|
78
|
+
if (flags.json) {
|
|
79
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
process.stdout.write(formatInstallResult(result, flags.dir !== undefined));
|
|
83
|
+
}
|
|
84
|
+
}
|
package/src/commands/config.ts
CHANGED
|
@@ -29,6 +29,7 @@ export default class Config extends Command {
|
|
|
29
29
|
|
|
30
30
|
static flags = {
|
|
31
31
|
json: Flags.boolean({ description: "Output JSON" }),
|
|
32
|
+
"show-secrets": Flags.boolean({ description: "Show secret-like setting values without redaction (unsafe)" }),
|
|
32
33
|
};
|
|
33
34
|
|
|
34
35
|
async run(): Promise<void> {
|
|
@@ -42,6 +43,7 @@ export default class Config extends Command {
|
|
|
42
43
|
value,
|
|
43
44
|
flags: {
|
|
44
45
|
json: flags.json,
|
|
46
|
+
showSecrets: flags["show-secrets"],
|
|
45
47
|
},
|
|
46
48
|
};
|
|
47
49
|
|
|
@@ -8,6 +8,7 @@ export default class DeepInterview extends Command {
|
|
|
8
8
|
quick: Flags.boolean({ description: "Seed a quick deep-interview run" }),
|
|
9
9
|
standard: Flags.boolean({ description: "Seed a standard deep-interview run" }),
|
|
10
10
|
deep: Flags.boolean({ description: "Seed a deep deep-interview run" }),
|
|
11
|
+
trace: Flags.boolean({ description: "Run a bounded trace evidence pre-step before interview questions" }),
|
|
11
12
|
threshold: Flags.string({ description: "Override ambiguity threshold for kickoff" }),
|
|
12
13
|
"threshold-source": Flags.string({ description: "Describe the threshold override source" }),
|
|
13
14
|
"session-id": Flags.string({
|
|
@@ -25,7 +26,7 @@ export default class DeepInterview extends Command {
|
|
|
25
26
|
json: Flags.boolean({ description: "Output JSON" }),
|
|
26
27
|
};
|
|
27
28
|
static examples = [
|
|
28
|
-
'$ gjc deep-interview --standard "<idea>"',
|
|
29
|
+
'$ gjc deep-interview --trace --standard "<idea>"',
|
|
29
30
|
"$ gjc deep-interview --write --stage final --slug my-feature --spec ./final-spec.md",
|
|
30
31
|
"$ gjc deep-interview --write --stage final --slug my-feature --spec ./final-spec.md --deliberate",
|
|
31
32
|
];
|
package/src/config/file-lock.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Stats } from "node:fs";
|
|
1
2
|
import * as fs from "node:fs/promises";
|
|
2
3
|
import { isEnoent } from "@gajae-code/utils/fs-error";
|
|
3
4
|
|
|
@@ -22,9 +23,10 @@ function getLockPath(filePath: string): string {
|
|
|
22
23
|
return `${filePath}.lock`;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
async function writeLockInfo(lockPath: string): Promise<
|
|
26
|
+
async function writeLockInfo(lockPath: string): Promise<LockInfo> {
|
|
26
27
|
const info: LockInfo = { pid: process.pid, timestamp: Date.now() };
|
|
27
28
|
await Bun.write(`${lockPath}/info`, JSON.stringify(info));
|
|
29
|
+
return info;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
async function readLockInfo(lockPath: string): Promise<LockInfo | null> {
|
|
@@ -51,17 +53,30 @@ export interface FileLockOwnerToken {
|
|
|
51
53
|
timestamp: number;
|
|
52
54
|
}
|
|
53
55
|
|
|
54
|
-
/** Outcome of a guarded
|
|
56
|
+
/** Outcome of a guarded lock-dir removal attempt (`removeFileLockDirForGc`). */
|
|
55
57
|
export type FileLockGcRemoval = "removed" | "owner_changed" | "missing";
|
|
56
58
|
|
|
59
|
+
interface LockDirStatToken {
|
|
60
|
+
dev: number;
|
|
61
|
+
ino: number;
|
|
62
|
+
mtimeMs: number;
|
|
63
|
+
ctimeMs: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
type LockStaleSnapshot =
|
|
67
|
+
| { stale: false }
|
|
68
|
+
| { stale: true; owner: FileLockOwnerToken }
|
|
69
|
+
| { stale: true; owner: null; stat: LockDirStatToken };
|
|
70
|
+
|
|
57
71
|
/**
|
|
58
72
|
* @internal
|
|
59
|
-
* Fail-closed removal of a
|
|
60
|
-
* token as close to the unlink as possible
|
|
61
|
-
* STILL holds the exact `{pid, timestamp}`
|
|
73
|
+
* Fail-closed removal of a lock dir whose owner is expected to be dead or
|
|
74
|
+
* finished. Re-reads the on-disk owner token as close to the unlink as possible
|
|
75
|
+
* and only deletes the dir when it STILL holds the exact `{pid, timestamp}`
|
|
76
|
+
* identity the caller observed.
|
|
62
77
|
*
|
|
63
|
-
* Closes
|
|
64
|
-
*
|
|
78
|
+
* Closes stale-cleanup TOCTOU windows (#606): between a dead/stale re-read and
|
|
79
|
+
* the unlink, a live process can reclaim a stale lock at the same path
|
|
65
80
|
* (`acquireLock` rms the stale dir, then re-`mkdir`s and rewrites `info` with a
|
|
66
81
|
* fresh pid+timestamp). Deleting by path alone would reap that LIVE lock. Any
|
|
67
82
|
* mismatch (`owner_changed`) or absent/unreadable info (`missing` — e.g. a
|
|
@@ -99,14 +114,28 @@ function ownerLiveness(pid: number): OwnerLiveness {
|
|
|
99
114
|
}
|
|
100
115
|
}
|
|
101
116
|
|
|
102
|
-
|
|
117
|
+
function statToken(stats: Stats): LockDirStatToken {
|
|
118
|
+
return {
|
|
119
|
+
dev: stats.dev,
|
|
120
|
+
ino: stats.ino,
|
|
121
|
+
mtimeMs: stats.mtimeMs,
|
|
122
|
+
ctimeMs: stats.ctimeMs,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function sameStatToken(a: LockDirStatToken, b: LockDirStatToken): boolean {
|
|
127
|
+
return a.dev === b.dev && a.ino === b.ino && a.mtimeMs === b.mtimeMs && a.ctimeMs === b.ctimeMs;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function staleLockSnapshot(lockPath: string, staleMs: number): Promise<LockStaleSnapshot> {
|
|
103
131
|
const info = await readLockInfo(lockPath);
|
|
104
132
|
if (!info) {
|
|
105
133
|
try {
|
|
106
134
|
const stats = await fs.stat(lockPath);
|
|
107
|
-
|
|
135
|
+
if (Date.now() - stats.mtimeMs <= staleMs) return { stale: false };
|
|
136
|
+
return { stale: true, owner: null, stat: statToken(stats) };
|
|
108
137
|
} catch (err) {
|
|
109
|
-
if (isEnoent(err)) return false;
|
|
138
|
+
if (isEnoent(err)) return { stale: false };
|
|
110
139
|
throw err;
|
|
111
140
|
}
|
|
112
141
|
}
|
|
@@ -115,53 +144,63 @@ async function isLockStale(lockPath: string, staleMs: number): Promise<boolean>
|
|
|
115
144
|
// not have its lock stolen (#652). Reclaim a dead owner immediately. Only when owner
|
|
116
145
|
// liveness is indeterminate do we fall back to the staleMs elapsed-time heuristic.
|
|
117
146
|
const liveness = ownerLiveness(info.pid);
|
|
118
|
-
if (liveness === "
|
|
119
|
-
if (liveness === "
|
|
120
|
-
|
|
147
|
+
if (liveness === "alive") return { stale: false };
|
|
148
|
+
if (liveness === "dead" || Date.now() - info.timestamp > staleMs) {
|
|
149
|
+
return { stale: true, owner: { pid: info.pid, timestamp: info.timestamp } };
|
|
150
|
+
}
|
|
151
|
+
return { stale: false };
|
|
121
152
|
}
|
|
122
153
|
|
|
123
|
-
async function
|
|
154
|
+
async function removeStaleLockForAcquire(lockPath: string, snapshot: LockStaleSnapshot): Promise<boolean> {
|
|
155
|
+
if (!snapshot.stale) return false;
|
|
156
|
+
if (snapshot.owner) {
|
|
157
|
+
return (await removeFileLockDirForGc(lockPath, snapshot.owner)) === "removed";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const currentInfo = await readLockInfo(lockPath);
|
|
161
|
+
if (currentInfo) return false;
|
|
124
162
|
try {
|
|
125
|
-
await fs.
|
|
126
|
-
|
|
163
|
+
const currentStats = await fs.stat(lockPath);
|
|
164
|
+
if (!sameStatToken(statToken(currentStats), snapshot.stat)) return false;
|
|
165
|
+
await fs.rm(lockPath, { recursive: true, force: true });
|
|
127
166
|
return true;
|
|
167
|
+
} catch (err) {
|
|
168
|
+
if (isEnoent(err)) return false;
|
|
169
|
+
throw err;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function tryAcquireLock(lockPath: string): Promise<LockInfo | null> {
|
|
174
|
+
try {
|
|
175
|
+
await fs.mkdir(lockPath);
|
|
176
|
+
return await writeLockInfo(lockPath);
|
|
128
177
|
} catch (error) {
|
|
129
178
|
if ((error as NodeJS.ErrnoException).code === "EEXIST") {
|
|
130
|
-
return
|
|
179
|
+
return null;
|
|
131
180
|
}
|
|
132
181
|
throw error;
|
|
133
182
|
}
|
|
134
183
|
}
|
|
135
184
|
|
|
136
|
-
async function releaseLock(lockPath: string): Promise<void> {
|
|
185
|
+
async function releaseLock(lockPath: string, owner: FileLockOwnerToken): Promise<void> {
|
|
137
186
|
try {
|
|
138
|
-
await
|
|
187
|
+
await removeFileLockDirForGc(lockPath, owner);
|
|
139
188
|
} catch {
|
|
140
189
|
// Ignore errors on release
|
|
141
190
|
}
|
|
142
191
|
}
|
|
143
|
-
|
|
144
|
-
async function lockExists(lockPath: string): Promise<boolean> {
|
|
145
|
-
try {
|
|
146
|
-
await fs.stat(lockPath);
|
|
147
|
-
return true;
|
|
148
|
-
} catch (err) {
|
|
149
|
-
if (isEnoent(err)) return false;
|
|
150
|
-
throw err;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
192
|
async function acquireLock(filePath: string, options: FileLockOptions = {}): Promise<() => Promise<void>> {
|
|
155
193
|
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
156
194
|
const lockPath = getLockPath(filePath);
|
|
157
195
|
|
|
158
196
|
for (let attempt = 0; attempt < opts.retries; attempt++) {
|
|
159
|
-
|
|
160
|
-
|
|
197
|
+
const owner = await tryAcquireLock(lockPath);
|
|
198
|
+
if (owner) {
|
|
199
|
+
return () => releaseLock(lockPath, owner);
|
|
161
200
|
}
|
|
162
201
|
|
|
163
|
-
|
|
164
|
-
|
|
202
|
+
const stale = await staleLockSnapshot(lockPath, opts.staleMs);
|
|
203
|
+
if (await removeStaleLockForAcquire(lockPath, stale)) {
|
|
165
204
|
continue;
|
|
166
205
|
}
|
|
167
206
|
|
|
@@ -134,8 +134,8 @@ export const KEYBINDINGS = {
|
|
|
134
134
|
description: "Queue message for next turn",
|
|
135
135
|
},
|
|
136
136
|
"app.message.dequeue": {
|
|
137
|
-
defaultKeys: "alt+up",
|
|
138
|
-
description: "
|
|
137
|
+
defaultKeys: ["alt+up", "alt+down"],
|
|
138
|
+
description: "Select queued message to edit",
|
|
139
139
|
},
|
|
140
140
|
"app.clipboard.pasteImage": {
|
|
141
141
|
defaultKeys: process.platform === "win32" ? "alt+v" : "ctrl+v",
|
|
@@ -76,6 +76,34 @@ export interface MaterializeModelProfileAssignmentOptions {
|
|
|
76
76
|
selector: string;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
export interface MaterializeModelProfileAssignmentsOptions {
|
|
80
|
+
session: Pick<
|
|
81
|
+
ModelProfileActivationSession,
|
|
82
|
+
"model" | "thinkingLevel" | "setActiveModelProfile" | "getActiveModelProfile"
|
|
83
|
+
>;
|
|
84
|
+
settings: Pick<Settings, "clearOverride" | "get" | "override" | "set">;
|
|
85
|
+
assignments: ReadonlyMap<GjcModelAssignmentTargetId, string> | Partial<Record<GjcModelAssignmentTargetId, string>>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isReadonlyAssignmentMap(
|
|
89
|
+
assignments: ReadonlyMap<GjcModelAssignmentTargetId, string> | Partial<Record<GjcModelAssignmentTargetId, string>>,
|
|
90
|
+
): assignments is ReadonlyMap<GjcModelAssignmentTargetId, string> {
|
|
91
|
+
return typeof (assignments as { entries?: unknown }).entries === "function";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function getMaterializedAssignments(
|
|
95
|
+
assignments: ReadonlyMap<GjcModelAssignmentTargetId, string> | Partial<Record<GjcModelAssignmentTargetId, string>>,
|
|
96
|
+
): Array<[GjcModelAssignmentTargetId, string]> {
|
|
97
|
+
if (isReadonlyAssignmentMap(assignments)) return [...assignments.entries()];
|
|
98
|
+
const assignmentRecord: Partial<Record<GjcModelAssignmentTargetId, string>> = assignments;
|
|
99
|
+
const result: Array<[GjcModelAssignmentTargetId, string]> = [];
|
|
100
|
+
for (const role of Object.keys(assignmentRecord) as GjcModelAssignmentTargetId[]) {
|
|
101
|
+
const selector = assignmentRecord[role];
|
|
102
|
+
if (selector !== undefined) result.push([role, selector]);
|
|
103
|
+
}
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
|
|
79
107
|
export function materializeActiveModelProfileAssignment(options: MaterializeModelProfileAssignmentOptions): boolean {
|
|
80
108
|
const activeProfile = options.session.getActiveModelProfile?.() ?? options.settings.get("modelProfile.default");
|
|
81
109
|
if (!activeProfile) return false;
|
|
@@ -109,6 +137,43 @@ export function materializeActiveModelProfileAssignment(options: MaterializeMode
|
|
|
109
137
|
return true;
|
|
110
138
|
}
|
|
111
139
|
|
|
140
|
+
export function materializeActiveModelProfileAssignments(options: MaterializeModelProfileAssignmentsOptions): boolean {
|
|
141
|
+
const activeProfile = options.session.getActiveModelProfile?.() ?? options.settings.get("modelProfile.default");
|
|
142
|
+
if (!activeProfile) return false;
|
|
143
|
+
|
|
144
|
+
const materializedAssignments = getMaterializedAssignments(options.assignments);
|
|
145
|
+
if (materializedAssignments.length === 0) return true;
|
|
146
|
+
|
|
147
|
+
const nextModelRoles = { ...options.settings.get("modelRoles") };
|
|
148
|
+
const nextAgentModelOverrides = { ...options.settings.get("task.agentModelOverrides") };
|
|
149
|
+
const includesDefault = materializedAssignments.some(([role]) => role === "default");
|
|
150
|
+
|
|
151
|
+
if (!includesDefault && !nextModelRoles.default && options.session.model) {
|
|
152
|
+
nextModelRoles.default = formatModelSelectorValue(
|
|
153
|
+
`${options.session.model.provider}/${options.session.model.id}`,
|
|
154
|
+
options.session.thinkingLevel,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
for (const [role, selector] of materializedAssignments) {
|
|
159
|
+
const target = GJC_MODEL_ASSIGNMENT_TARGETS[role];
|
|
160
|
+
if (target.settingsPath === "modelRoles") {
|
|
161
|
+
nextModelRoles[role] = selector;
|
|
162
|
+
} else {
|
|
163
|
+
nextAgentModelOverrides[role] = selector;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
options.settings.set("modelRoles", nextModelRoles);
|
|
168
|
+
options.settings.set("task.agentModelOverrides", nextAgentModelOverrides);
|
|
169
|
+
options.settings.set("modelProfile.default", undefined);
|
|
170
|
+
options.settings.clearOverride("modelProfile.default");
|
|
171
|
+
options.settings.override("modelRoles", nextModelRoles);
|
|
172
|
+
options.settings.override("task.agentModelOverrides", nextAgentModelOverrides);
|
|
173
|
+
options.session.setActiveModelProfile?.(undefined);
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
|
|
112
177
|
export function formatModelProfileCredentialError(profileLabel: string, providers: readonly string[]): string {
|
|
113
178
|
return `Model profile "${profileLabel}" requires credentials for: ${providers.join(", ")}. Run /login and configure the missing provider(s), then retry.`;
|
|
114
179
|
}
|