@gajae-code/coding-agent 0.8.0 → 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.
Files changed (116) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/types/cli/completion-cli.d.ts +41 -0
  3. package/dist/types/cli/config-cli.d.ts +1 -0
  4. package/dist/types/cli/notify-cli.d.ts +7 -0
  5. package/dist/types/cli.d.ts +124 -1
  6. package/dist/types/commands/completion.d.ts +29 -0
  7. package/dist/types/commands/config.d.ts +3 -0
  8. package/dist/types/commands/deep-interview.d.ts +3 -0
  9. package/dist/types/config/file-lock.d.ts +7 -6
  10. package/dist/types/config/keybindings.d.ts +2 -2
  11. package/dist/types/config/model-profile-activation.d.ts +6 -0
  12. package/dist/types/config/settings.d.ts +8 -0
  13. package/dist/types/extensibility/extensions/runner.d.ts +2 -1
  14. package/dist/types/extensibility/extensions/types.d.ts +8 -0
  15. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +39 -0
  16. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +10 -1
  17. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -1
  18. package/dist/types/gjc-runtime/deep-interview-state.d.ts +12 -0
  19. package/dist/types/gjc-runtime/team-runtime.d.ts +2 -1
  20. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
  21. package/dist/types/main.d.ts +4 -1
  22. package/dist/types/modes/components/model-selector.d.ts +4 -0
  23. package/dist/types/modes/components/queued-message-selector.d.ts +12 -0
  24. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  25. package/dist/types/modes/controllers/input-controller.d.ts +1 -3
  26. package/dist/types/modes/print-mode.d.ts +13 -1
  27. package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -1
  28. package/dist/types/notifications/config.d.ts +8 -1
  29. package/dist/types/notifications/daemon-paths.d.ts +1 -0
  30. package/dist/types/notifications/index.d.ts +6 -3
  31. package/dist/types/notifications/lifecycle-commands.d.ts +11 -3
  32. package/dist/types/notifications/telegram-daemon.d.ts +18 -3
  33. package/dist/types/notifications/telegram-reference.d.ts +0 -5
  34. package/dist/types/runtime-api-key.d.ts +7 -0
  35. package/dist/types/session/agent-session.d.ts +10 -0
  36. package/dist/types/session/agent-storage.d.ts +1 -0
  37. package/dist/types/task/executor.d.ts +2 -0
  38. package/dist/types/tools/computer.d.ts +2 -2
  39. package/dist/types/tools/skill.d.ts +1 -1
  40. package/dist/types/workflow/workflow-intent-diff.d.ts +8 -2
  41. package/dist/types/workflow/workflow-intent-report-consumer.d.ts +50 -0
  42. package/dist/types/workflow/workflow-intent-report-schema.d.ts +2 -0
  43. package/dist/types/workflow/workflow-intent-report.d.ts +48 -0
  44. package/package.json +7 -7
  45. package/src/cli/completion-cli.ts +234 -0
  46. package/src/cli/config-cli.ts +37 -9
  47. package/src/cli/fast-help.ts +1 -0
  48. package/src/cli/notify-cli.ts +91 -7
  49. package/src/cli.ts +3 -1
  50. package/src/commands/completion.ts +84 -0
  51. package/src/commands/config.ts +2 -0
  52. package/src/commands/deep-interview.ts +2 -1
  53. package/src/config/file-lock.ts +73 -34
  54. package/src/config/keybindings.ts +2 -2
  55. package/src/config/model-profile-activation.ts +65 -0
  56. package/src/config/model-registry.ts +14 -2
  57. package/src/config/settings.ts +22 -0
  58. package/src/coordinator-mcp/server.ts +67 -12
  59. package/src/defaults/gjc/skills/deep-interview/SKILL.md +38 -5
  60. package/src/defaults/gjc/skills/ralplan/SKILL.md +15 -11
  61. package/src/defaults/gjc/skills/team/SKILL.md +1 -1
  62. package/src/extensibility/extensions/runner.ts +2 -0
  63. package/src/extensibility/extensions/types.ts +9 -0
  64. package/src/extensibility/skills.ts +30 -2
  65. package/src/gjc-runtime/deep-interview-ambiguity.ts +206 -0
  66. package/src/gjc-runtime/deep-interview-recorder.ts +62 -8
  67. package/src/gjc-runtime/deep-interview-runtime.ts +210 -2
  68. package/src/gjc-runtime/deep-interview-state.ts +13 -0
  69. package/src/gjc-runtime/session-state-sidecar.ts +59 -11
  70. package/src/gjc-runtime/state-runtime.ts +12 -2
  71. package/src/gjc-runtime/team-runtime.ts +23 -0
  72. package/src/gjc-runtime/ultragoal-runtime.ts +73 -19
  73. package/src/hooks/native-skill-hook.ts +29 -0
  74. package/src/internal-urls/docs-index.generated.ts +2 -2
  75. package/src/main.ts +52 -41
  76. package/src/modes/acp/acp-agent.ts +7 -6
  77. package/src/modes/components/model-selector.ts +54 -11
  78. package/src/modes/components/queued-message-selector.ts +92 -0
  79. package/src/modes/components/settings-selector.ts +4 -0
  80. package/src/modes/components/welcome.ts +160 -44
  81. package/src/modes/controllers/command-controller.ts +4 -4
  82. package/src/modes/controllers/extension-ui-controller.ts +4 -10
  83. package/src/modes/controllers/input-controller.ts +186 -25
  84. package/src/modes/controllers/selector-controller.ts +122 -27
  85. package/src/modes/interactive-mode.ts +43 -16
  86. package/src/modes/print-mode.ts +43 -4
  87. package/src/modes/prompt-action-autocomplete.ts +24 -13
  88. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  89. package/src/modes/utils/ui-helpers.ts +2 -2
  90. package/src/notifications/config.ts +9 -1
  91. package/src/notifications/daemon-paths.ts +2 -0
  92. package/src/notifications/index.ts +39 -9
  93. package/src/notifications/lifecycle-commands.ts +37 -7
  94. package/src/notifications/lifecycle-orchestrator.ts +19 -6
  95. package/src/notifications/telegram-daemon-control.ts +3 -1
  96. package/src/notifications/telegram-daemon.ts +231 -43
  97. package/src/notifications/telegram-reference.ts +0 -16
  98. package/src/prompts/system/system-prompt.md +3 -0
  99. package/src/prompts/tools/computer.md +2 -2
  100. package/src/runtime-api-key.ts +16 -0
  101. package/src/sdk.ts +20 -6
  102. package/src/session/agent-session.ts +225 -31
  103. package/src/session/agent-storage.ts +4 -0
  104. package/src/session/session-manager.ts +44 -2
  105. package/src/slash-commands/builtin-registry.ts +153 -71
  106. package/src/task/executor.ts +7 -4
  107. package/src/task/index.ts +9 -3
  108. package/src/task/output-manager.ts +16 -6
  109. package/src/task/receipt.ts +26 -7
  110. package/src/tools/computer.ts +246 -43
  111. package/src/tools/skill.ts +1 -1
  112. package/src/tools/telegram-send.ts +6 -0
  113. package/src/workflow/workflow-intent-diff.ts +29 -6
  114. package/src/workflow/workflow-intent-report-consumer.ts +221 -0
  115. package/src/workflow/workflow-intent-report-schema.ts +129 -0
  116. package/src/workflow/workflow-intent-report.ts +135 -0
@@ -0,0 +1,234 @@
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 { ArgDescriptor, CommandCtor, CommandEntry, FlagDescriptor } from "@gajae-code/utils/cli";
5
+
6
+ export type FigName = string | string[];
7
+
8
+ export interface FigArg {
9
+ name: string;
10
+ description?: string;
11
+ isOptional?: boolean;
12
+ isVariadic?: boolean;
13
+ suggestions?: string[];
14
+ }
15
+
16
+ export interface FigOption {
17
+ name: FigName;
18
+ description?: string;
19
+ args?: FigArg;
20
+ isRepeatable?: boolean;
21
+ }
22
+
23
+ export interface FigSubcommand {
24
+ name: FigName;
25
+ description?: string;
26
+ options?: FigOption[];
27
+ args?: FigArg | FigArg[];
28
+ }
29
+
30
+ export interface FigSpec extends FigSubcommand {
31
+ name: string;
32
+ subcommands?: FigSubcommand[];
33
+ }
34
+
35
+ export type InshellisenseIndexStatus = "created" | "updated" | "already-compatible" | "overwritten";
36
+
37
+ export interface InstallGjcInshellisenseSpecOptions {
38
+ dir?: string;
39
+ force?: boolean;
40
+ }
41
+
42
+ export interface InstallGjcInshellisenseSpecResult {
43
+ directory: string;
44
+ specPath: string;
45
+ indexPath: string;
46
+ indexStatus: InshellisenseIndexStatus;
47
+ }
48
+
49
+ const GENERATED_INDEX_MARKER = "// Generated by gjc completion inshellisense";
50
+ const BUILTIN_ROOT_OPTIONS: FigOption[] = [
51
+ { name: ["-h", "--help"], description: "Show help" },
52
+ { name: ["-v", "--version"], description: "Show version" },
53
+ ];
54
+ const BUILTIN_COMMAND_OPTIONS: FigOption[] = [{ name: ["-h", "--help"], description: "Show help" }];
55
+
56
+ export function defaultInshellisenseSpecDir(homeDir: string = os.homedir()): string {
57
+ return path.join(homeDir, ".fig", "autocomplete", "build");
58
+ }
59
+
60
+ function nameList(name: FigName): string[] {
61
+ return Array.isArray(name) ? name : [name];
62
+ }
63
+
64
+ function optionKey(option: FigOption): string {
65
+ return nameList(option.name).join("\u0000");
66
+ }
67
+
68
+ function combineOptions(primary: FigOption[], secondary: FigOption[]): FigOption[] {
69
+ const seen = new Set<string>();
70
+ const combined: FigOption[] = [];
71
+ for (const option of [...primary, ...secondary]) {
72
+ const key = optionKey(option);
73
+ if (seen.has(key)) continue;
74
+ seen.add(key);
75
+ combined.push(option);
76
+ }
77
+ return combined;
78
+ }
79
+
80
+ function commandEntryName(entry: CommandEntry): FigName {
81
+ const names = [entry.name, ...(entry.aliases ?? [])];
82
+ return names.length === 1 ? names[0]! : names;
83
+ }
84
+
85
+ function flagName(name: string, descriptor: FlagDescriptor): FigName {
86
+ const longName = `--${name}`;
87
+ if (!descriptor.char) return longName;
88
+ return [`-${descriptor.char}`, longName];
89
+ }
90
+
91
+ function argSuggestions(options: readonly string[] | undefined): string[] | undefined {
92
+ return options && options.length > 0 ? [...options] : undefined;
93
+ }
94
+
95
+ function optionArgName(name: string, descriptor: FlagDescriptor): string {
96
+ if (descriptor.options && descriptor.options.length > 0) return name;
97
+ return descriptor.kind === "integer" ? "int" : "value";
98
+ }
99
+
100
+ function figOptionFromFlag(name: string, descriptor: FlagDescriptor): FigOption {
101
+ const option: FigOption = {
102
+ name: flagName(name, descriptor),
103
+ };
104
+ if (descriptor.description) option.description = descriptor.description;
105
+ if (descriptor.multiple) option.isRepeatable = true;
106
+ if (descriptor.kind !== "boolean") {
107
+ const arg: FigArg = { name: optionArgName(name, descriptor) };
108
+ const suggestions = argSuggestions(descriptor.options);
109
+ if (suggestions) arg.suggestions = suggestions;
110
+ option.args = arg;
111
+ }
112
+ return option;
113
+ }
114
+
115
+ function figOptionsFromFlags(flags: Record<string, FlagDescriptor> | undefined): FigOption[] {
116
+ if (!flags) return [];
117
+ return Object.entries(flags).map(([name, descriptor]) => figOptionFromFlag(name, descriptor));
118
+ }
119
+
120
+ function figArgFromDescriptor(name: string, descriptor: ArgDescriptor): FigArg {
121
+ const arg: FigArg = { name };
122
+ if (descriptor.description) arg.description = descriptor.description;
123
+ if (!descriptor.required) arg.isOptional = true;
124
+ if (descriptor.multiple) arg.isVariadic = true;
125
+ const suggestions = argSuggestions(descriptor.options);
126
+ if (suggestions) arg.suggestions = suggestions;
127
+ return arg;
128
+ }
129
+
130
+ function figArgsFromDescriptors(args: Record<string, ArgDescriptor> | undefined): FigArg | FigArg[] | undefined {
131
+ if (!args) return undefined;
132
+ const values = Object.entries(args).map(([name, descriptor]) => figArgFromDescriptor(name, descriptor));
133
+ if (values.length === 0) return undefined;
134
+ return values.length === 1 ? values[0] : values;
135
+ }
136
+
137
+ function commandToSubcommand(entry: CommandEntry, command: CommandCtor): FigSubcommand {
138
+ const subcommand: FigSubcommand = { name: commandEntryName(entry) };
139
+ if (command.description) subcommand.description = command.description;
140
+ const options = combineOptions(BUILTIN_COMMAND_OPTIONS, figOptionsFromFlags(command.flags));
141
+ if (options.length > 0) subcommand.options = options;
142
+ const args = figArgsFromDescriptors(command.args);
143
+ if (args) subcommand.args = args;
144
+ return subcommand;
145
+ }
146
+
147
+ export async function buildGjcFigSpec(entries: CommandEntry[], rootCommandOverride?: CommandCtor): Promise<FigSpec> {
148
+ const subcommands: FigSubcommand[] = [];
149
+ let rootCommand: CommandCtor | undefined = rootCommandOverride;
150
+
151
+ for (const entry of entries) {
152
+ if (entry.name === "launch" && rootCommandOverride) {
153
+ continue;
154
+ }
155
+ let command: CommandCtor;
156
+ try {
157
+ command = await entry.load();
158
+ } catch {
159
+ if (entry.name !== "launch") {
160
+ subcommands.push({ name: commandEntryName(entry) });
161
+ }
162
+ continue;
163
+ }
164
+ if (entry.name === "launch") {
165
+ rootCommand = command;
166
+ continue;
167
+ }
168
+ if (command.hidden) continue;
169
+ subcommands.push(commandToSubcommand(entry, command));
170
+ }
171
+
172
+ const spec: FigSpec = {
173
+ name: "gjc",
174
+ description: rootCommand?.description ?? "Gajae Code CLI",
175
+ };
176
+ const rootOptions = combineOptions(BUILTIN_ROOT_OPTIONS, figOptionsFromFlags(rootCommand?.flags));
177
+ if (rootOptions.length > 0) spec.options = rootOptions;
178
+ const rootArgs = figArgsFromDescriptors(rootCommand?.args);
179
+ if (rootArgs) spec.args = rootArgs;
180
+ if (subcommands.length > 0) spec.subcommands = subcommands;
181
+ return spec;
182
+ }
183
+
184
+ export function renderFigSpecModule(spec: FigSpec): string {
185
+ return `const completionSpec = ${JSON.stringify(spec, null, "\t")};\n\nexport default completionSpec;\n`;
186
+ }
187
+
188
+ export function renderInshellisenseIndexModule(): string {
189
+ return `${GENERATED_INDEX_MARKER}\n\nexport default ["gjc"];\nexport const diffVersionedCompletions = [];\n`;
190
+ }
191
+
192
+ async function fileTextIfExists(filePath: string): Promise<string | undefined> {
193
+ try {
194
+ return await Bun.file(filePath).text();
195
+ } catch (error) {
196
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return undefined;
197
+ throw error;
198
+ }
199
+ }
200
+
201
+ function indexDeclaresGjc(content: string): boolean {
202
+ return /["']gjc["']/.test(content);
203
+ }
204
+
205
+ async function resolveIndexStatus(indexPath: string, force: boolean | undefined): Promise<InshellisenseIndexStatus> {
206
+ const existing = await fileTextIfExists(indexPath);
207
+ if (existing === undefined) return "created";
208
+ if (existing.includes(GENERATED_INDEX_MARKER)) {
209
+ return existing === renderInshellisenseIndexModule() ? "already-compatible" : "updated";
210
+ }
211
+ if (indexDeclaresGjc(existing)) return "already-compatible";
212
+ if (force) return "overwritten";
213
+ throw new Error(
214
+ `inshellisense index already exists at ${indexPath} and does not list gjc; rerun with --force to overwrite it or pass --dir to install into a GJC-only specs directory`,
215
+ );
216
+ }
217
+
218
+ export async function installGjcInshellisenseSpec(
219
+ spec: FigSpec,
220
+ options: InstallGjcInshellisenseSpecOptions = {},
221
+ ): Promise<InstallGjcInshellisenseSpecResult> {
222
+ const directory = path.resolve(options.dir ?? defaultInshellisenseSpecDir());
223
+ const specPath = path.join(directory, "gjc.js");
224
+ const indexPath = path.join(directory, "index.js");
225
+ const indexStatus = await resolveIndexStatus(indexPath, options.force);
226
+
227
+ await fs.mkdir(directory, { recursive: true });
228
+ await Bun.write(specPath, renderFigSpecModule(spec));
229
+ if (indexStatus !== "already-compatible") {
230
+ await Bun.write(indexPath, renderInshellisenseIndexModule());
231
+ }
232
+
233
+ return { directory, specPath, indexPath, indexStatus };
234
+ }
@@ -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: settings.get(def.path),
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 valueStr = formatValue(value);
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(JSON.stringify({ key: def.path, value, type: def.type, description: def.description }, null, 2));
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(value));
354
+ console.log(formatValue(displayValue));
334
355
  }
335
356
 
336
- async function handleSet(key: string | undefined, value: string | undefined, flags: { json?: boolean }): Promise<void> {
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: newValue }));
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(newValue)}`));
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:")}
@@ -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
@@ -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
- async function promptForToken(): Promise<string> {
174
- if (!process.stdin.isTTY) {
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
- const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
178
- try {
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
+ }
@@ -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
  ];