@gajae-code/coding-agent 0.12.18 → 0.12.20

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 (100) hide show
  1. package/CHANGELOG.md +26 -9
  2. package/dist/types/commands/gc.d.ts +4 -0
  3. package/dist/types/commands/sdk.d.ts +66 -0
  4. package/dist/types/config/settings-schema.d.ts +34 -0
  5. package/dist/types/gjc-runtime/gc-runtime.d.ts +97 -1
  6. package/dist/types/modes/acp/acp-agent.d.ts +33 -1
  7. package/dist/types/modes/components/bash-execution.d.ts +7 -0
  8. package/dist/types/modes/components/eval-execution.d.ts +7 -0
  9. package/dist/types/modes/utils/ui-helpers.d.ts +10 -1
  10. package/dist/types/sdk/acp/adapter.d.ts +9 -17
  11. package/dist/types/sdk/acp/index.d.ts +1 -1
  12. package/dist/types/sdk/broker/broker.d.ts +93 -1
  13. package/dist/types/sdk/broker/ensure.d.ts +17 -0
  14. package/dist/types/sdk/broker/index.d.ts +1 -0
  15. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +10 -1
  16. package/dist/types/sdk/broker/lifecycle.d.ts +81 -2
  17. package/dist/types/sdk/broker/startup-budget.d.ts +44 -0
  18. package/dist/types/sdk/bus/chat-daemon-control.d.ts +7 -1
  19. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +11 -1
  20. package/dist/types/sdk/bus/discord-daemon.d.ts +1 -0
  21. package/dist/types/sdk/bus/index.d.ts +20 -9
  22. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -6
  23. package/dist/types/sdk/bus/slack-daemon.d.ts +2 -2
  24. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -2
  25. package/dist/types/sdk/host/query/revision-store.d.ts +5 -0
  26. package/dist/types/sdk/host/session-runtime.d.ts +4 -0
  27. package/dist/types/sdk/lifecycle-session.d.ts +13 -2
  28. package/dist/types/sdk/prompt-failure.d.ts +13 -0
  29. package/dist/types/sdk/prompt-watchdog.d.ts +82 -0
  30. package/dist/types/sdk/session-reconnect.d.ts +20 -0
  31. package/dist/types/sdk/startup-capability.d.ts +1 -1
  32. package/dist/types/session/agent-session.d.ts +14 -4
  33. package/dist/types/session/blob-store.d.ts +32 -0
  34. package/dist/types/session/internal/managed-session-storage.d.ts +40 -2
  35. package/dist/types/session/internal/session-memory-sidecar.d.ts +881 -0
  36. package/dist/types/session/session-manager.d.ts +301 -27
  37. package/dist/types/session/session-storage.d.ts +170 -0
  38. package/dist/types/task/executor.d.ts +2 -1
  39. package/dist/types/tools/bash.d.ts +1 -0
  40. package/dist/types/tui/output-block.d.ts +6 -0
  41. package/package.json +8 -8
  42. package/src/cli/args.ts +75 -34
  43. package/src/commands/acp.ts +7 -1
  44. package/src/commands/gc.ts +31 -2
  45. package/src/commands/sdk.ts +141 -7
  46. package/src/config/settings-schema.ts +43 -0
  47. package/src/coordinator-mcp/server.ts +6 -1
  48. package/src/export/html/index.ts +83 -39
  49. package/src/gjc-runtime/gc-runtime.ts +1283 -4
  50. package/src/internal-urls/docs-index.generated.ts +1 -1
  51. package/src/internal-urls/local-protocol.ts +14 -0
  52. package/src/main.ts +40 -9
  53. package/src/modes/acp/acp-agent.ts +564 -37
  54. package/src/modes/acp/terminal-auth.ts +2 -2
  55. package/src/modes/components/bash-execution.ts +14 -0
  56. package/src/modes/components/eval-execution.ts +14 -0
  57. package/src/modes/controllers/command-controller.ts +63 -8
  58. package/src/modes/utils/ui-helpers.ts +81 -11
  59. package/src/sdk/acp/adapter.ts +46 -45
  60. package/src/sdk/acp/index.ts +1 -1
  61. package/src/sdk/broker/broker.ts +392 -11
  62. package/src/sdk/broker/ensure.ts +131 -61
  63. package/src/sdk/broker/index.ts +1 -0
  64. package/src/sdk/broker/lifecycle-ledger.ts +59 -5
  65. package/src/sdk/broker/lifecycle.ts +278 -63
  66. package/src/sdk/broker/startup-budget.ts +103 -0
  67. package/src/sdk/bus/chat-daemon-control.ts +9 -3
  68. package/src/sdk/bus/chat-daemon-runtime.ts +565 -29
  69. package/src/sdk/bus/conversation-store.ts +1 -1
  70. package/src/sdk/bus/discord-daemon.ts +2 -1
  71. package/src/sdk/bus/discord-live-provider.ts +34 -6
  72. package/src/sdk/bus/index.ts +118 -20
  73. package/src/sdk/bus/kind-aware-reconciliation.ts +2 -1
  74. package/src/sdk/bus/prompt-reconciliation.ts +4 -9
  75. package/src/sdk/bus/slack-daemon.ts +91 -18
  76. package/src/sdk/bus/telegram-daemon-contract.ts +4 -2
  77. package/src/sdk/bus/topic-registry.ts +7 -1
  78. package/src/sdk/cli/session-cli.ts +8 -1
  79. package/src/sdk/host/query/handlers.ts +1 -1
  80. package/src/sdk/host/query/revision-store.ts +48 -12
  81. package/src/sdk/host/session-runtime.test.ts +50 -0
  82. package/src/sdk/host/session-runtime.ts +12 -2
  83. package/src/sdk/lifecycle-session.ts +26 -3
  84. package/src/sdk/mcp/server.ts +6 -1
  85. package/src/sdk/prompt-failure.ts +42 -0
  86. package/src/sdk/prompt-watchdog.ts +162 -0
  87. package/src/sdk/session-reconnect.ts +43 -0
  88. package/src/sdk/startup-capability.ts +13 -4
  89. package/src/session/agent-session.ts +525 -222
  90. package/src/session/blob-store.ts +89 -0
  91. package/src/session/internal/managed-session-scope.ts +36 -26
  92. package/src/session/internal/managed-session-storage.ts +303 -32
  93. package/src/session/internal/session-memory-sidecar.ts +2160 -0
  94. package/src/session/session-manager.ts +7027 -394
  95. package/src/session/session-storage.ts +1134 -11
  96. package/src/task/executor.ts +17 -4
  97. package/src/tools/bash.ts +109 -7
  98. package/src/tools/find.ts +55 -26
  99. package/src/tools/todo-contract.ts +40 -8
  100. package/src/tui/output-block.ts +11 -1
@@ -16,6 +16,22 @@ export interface SessionStorageSnapshot {
16
16
  bytes: Uint8Array;
17
17
  stat: SessionStorageStat;
18
18
  }
19
+ export interface SessionStorageExactReplacementExpectation {
20
+ readonly stat: SessionStorageStat;
21
+ readonly sha256: string;
22
+ }
23
+ /** Upper bound for one descriptor-validated recorded range read. */
24
+ export declare const SESSION_RANGE_READ_MAX_BYTES: number;
25
+ /**
26
+ * One bounded recorded-length read validated against a single opened descriptor.
27
+ * `bytes` is exactly `length` bytes from `[start, start + length)` of the same
28
+ * regular-file object; `stat` is the fresh post-read descriptor snapshot so the
29
+ * caller can compare dev/ino/nlink against the pathname before committing an index.
30
+ */
31
+ export interface SessionStorageRangeSnapshot {
32
+ stat: SessionStorageStat;
33
+ bytes: Uint8Array;
34
+ }
19
35
  /**
20
36
  * Four-state writer close lifecycle. Only a successful underlying close confirms
21
37
  * `closed`. A failure certified to have happened BEFORE the OS close was dispatched
@@ -93,6 +109,10 @@ export interface SessionStorageWriter {
93
109
  writeLineSync(line: string): void;
94
110
  flush(): Promise<void>;
95
111
  fsync(): Promise<void>;
112
+ /** Synchronously fsync all prior writes when the backend supports durable sidecar publication. */
113
+ fsyncSync?(): void;
114
+ /** Descriptor-bound identity captured from the still-open writer after fsync. */
115
+ statSync?(): SessionStorageStat;
96
116
  close(): Promise<void>;
97
117
  /**
98
118
  * Synchronously close the underlying descriptor. The certainty-aware close
@@ -107,6 +127,45 @@ export interface SessionStorageWriter {
107
127
  /** Stored error for non-success close states (`close_failed_retryable`/`close_unknown`). */
108
128
  getCloseError(): Error | undefined;
109
129
  }
130
+ /** Upper bound for one staged line (excluding the trailing newline). */
131
+ export declare const STAGED_WRITER_LINE_MAX_BYTES: number;
132
+ /** Upper bound for aggregated different-length patches buffered for the publish-time overlay pass. */
133
+ export declare const STAGED_WRITER_PATCH_LIMIT_BYTES: number;
134
+ export declare const STAGED_WRITER_PATCH_MAX_COUNT = 65536;
135
+ export declare const STAGED_MEMORY_WRITER_MAX_BYTES: number;
136
+ export declare const STAGED_MEMORY_WRITER_MAX_LINES: number;
137
+ /**
138
+ * Bounded staged streaming writer for one immutable one-shot destination (fork /
139
+ * capture). Lines are streamed to a sibling staging file; {@link publishNoReplace}
140
+ * atomically publishes the staged file only while the destination is still absent,
141
+ * so publication never materializes the whole file in memory. Different-length
142
+ * {@link patchLine} replacements are buffered (bounded) and applied by a second
143
+ * bounded streaming pass at publish time.
144
+ *
145
+ * `publishNoReplace` is reserved for immutable one-shot destinations and must never
146
+ * be used for the mutable `.spill.commit` marker (checked create/replace helpers
147
+ * exist for that path).
148
+ */
149
+ export interface StagedStreamingWriter {
150
+ /** Append one complete line; the writer adds the trailing newline. */
151
+ writeLine(bytes: Uint8Array): void;
152
+ /** Move the line cursor to `ordinal` (0-based) so a later patchLine targets it. */
153
+ seekToLine(ordinal: number): void;
154
+ /**
155
+ * Replace the line at `ordinal` with `bytes`. Same-length replacements are
156
+ * applied in place; different-length replacements are buffered (bounded) and
157
+ * applied by the publish-time overlay pass.
158
+ */
159
+ patchLine(ordinal: number, bytes: Uint8Array): void;
160
+ /** Hand buffered writes to the kernel (the staged descriptor is unbuffered). */
161
+ flush(): void;
162
+ /** Synchronize the staged file. */
163
+ fsync(): void;
164
+ /** Close the staged descriptor; required before {@link publishNoReplace}. */
165
+ closeSync(): void;
166
+ /** Atomically publish the staged file at the destination only while it is absent. */
167
+ publishNoReplace(): void;
168
+ }
110
169
  export interface SessionStorage {
111
170
  ensureDirSync(dir: string): void;
112
171
  existsSync(path: string): boolean;
@@ -134,6 +193,8 @@ export interface SessionStorage {
134
193
  writeText(path: string, content: string): Promise<void>;
135
194
  rename(path: string, nextPath: string): Promise<void>;
136
195
  renameSync(path: string, nextPath: string): void;
196
+ /** Replace only while the destination still has the expected exact identity and bytes. */
197
+ replaceExactSync?(sourcePath: string, destinationPath: string, expected: SessionStorageExactReplacementExpectation): boolean;
137
198
  unlink(path: string): Promise<void>;
138
199
  unlinkSync(path: string): void;
139
200
  deleteSessionWithArtifacts(sessionPath: string): Promise<void>;
@@ -145,6 +206,12 @@ export interface SessionStorage {
145
206
  */
146
207
  deleteSessionVerified?(target: VerifiedSessionDeleteTarget): Promise<VerifiedSessionDeleteResult>;
147
208
  openWriter(path: string, options?: SessionStorageWriterOpenOptions): SessionStorageWriter;
209
+ /** Bounded recorded-length read with descriptor identity validation (additive). */
210
+ readRangeSync?(path: string, start: number, length: number): SessionStorageRangeSnapshot;
211
+ /** Async bounded recorded-length read with descriptor identity validation (additive). */
212
+ readRange?(path: string, start: number, length: number): Promise<SessionStorageRangeSnapshot>;
213
+ /** Open a staged streaming writer for one immutable one-shot destination (additive). */
214
+ openStagedWriter?(path: string, options?: SessionStorageWriterOpenOptions): StagedStreamingWriter;
148
215
  }
149
216
  /** Exact authorization evidence for a transcript or artifact path. */
150
217
  export interface SessionStorageFileIdentity {
@@ -277,6 +344,42 @@ export type NativeDirectoryTreeSnapshot = {
277
344
  rootIno: string;
278
345
  entries: NativeDirectoryTreeEntry[];
279
346
  };
347
+ /** Physically observed commit-marker state; corrupt JSON is still `present`. */
348
+ export type SessionCommitMarkerState = {
349
+ kind: "missing";
350
+ } | {
351
+ kind: "present";
352
+ rawBytesSha256: string;
353
+ stat: SessionStorageStat;
354
+ };
355
+ /** Exact `present` expectation for one checked commit-marker replacement. */
356
+ export interface SessionCommitMarkerPresentExpectation {
357
+ /** SHA-256 of the exact raw marker bytes physically on disk (corrupt JSON included). */
358
+ rawBytesSha256: string;
359
+ /** Descriptor snapshot of the marker object expected to be replaced. */
360
+ descriptorIdentity: SessionStorageStat;
361
+ }
362
+ /** Snapshot one commit marker's physical state without granting write authority. */
363
+ export declare function readSessionCommitMarkerSync(storage: SessionStorage, markerPath: string): SessionCommitMarkerState;
364
+ /**
365
+ * Checked commit-marker create: publishes only while the marker is still `missing`.
366
+ * Temp + fsync + atomic create-if-absent + directory fsync (file backend); the
367
+ * in-memory backend mirrors the same missing-expectation abort. Leftover temps are
368
+ * removed on any failure. Runs inside the caller's persistence fence.
369
+ */
370
+ export declare function createSessionCommitMarkerCheckedSync(storage: SessionStorage, markerPath: string, bytes: Uint8Array, options?: {
371
+ securityContext?: SessionStorageSecurityContext;
372
+ }): void;
373
+ /**
374
+ * Checked commit-marker replace: replaces only on an exact `present` raw/hash +
375
+ * descriptor identity match (corrupt-present included). Temp + fsync + checked
376
+ * atomic rename + directory fsync (file backend); any mismatch aborts with the
377
+ * current marker untouched. The in-memory backend mirrors the same aborts. Runs
378
+ * inside the caller's persistence fence.
379
+ */
380
+ export declare function replaceSessionCommitMarkerCheckedSync(storage: SessionStorage, markerPath: string, bytes: Uint8Array, expected: SessionCommitMarkerPresentExpectation, options?: {
381
+ securityContext?: SessionStorageSecurityContext;
382
+ }): void;
280
383
  export declare class FileSessionStorage implements SessionStorage {
281
384
  #private;
282
385
  ensureDirSync(dir: string): void;
@@ -285,6 +388,15 @@ export declare class FileSessionStorage implements SessionStorage {
285
388
  readTextSync(fpath: string): string;
286
389
  readBytesSync(fpath: string): Uint8Array;
287
390
  readSnapshotSync(fpath: string): SessionStorageSnapshot;
391
+ /**
392
+ * Bounded recorded-length read with descriptor identity validation: opens one
393
+ * no-follow descriptor, verifies the requested range is fully present, reads
394
+ * exactly `length` bytes, and revalidates dev/ino/nlink on the same descriptor
395
+ * plus the pathname (append-only size growth is tolerated; an object swap is
396
+ * rejected). No path-based Bun Blob reads for managed authority.
397
+ */
398
+ readRangeSync(fpath: string, start: number, length: number): SessionStorageRangeSnapshot;
399
+ readRange(fpath: string, start: number, length: number): Promise<SessionStorageRangeSnapshot>;
288
400
  statSync(path: string): SessionStorageStat;
289
401
  listFilesSync(dir: string, pattern: string): string[];
290
402
  listFilesByMtime(dir: string, pattern: string): Promise<Array<{
@@ -298,9 +410,11 @@ export declare class FileSessionStorage implements SessionStorage {
298
410
  writeText(path: string, content: string): Promise<void>;
299
411
  rename(path: string, nextPath: string): Promise<void>;
300
412
  renameSync(path: string, nextPath: string): void;
413
+ replaceExactSync(sourcePath: string, destinationPath: string, expected: SessionStorageExactReplacementExpectation): boolean;
301
414
  unlink(path: string): Promise<void>;
302
415
  unlinkSync(path: string): void;
303
416
  openWriter(path: string, options?: SessionStorageWriterOpenOptions): SessionStorageWriter;
417
+ openStagedWriter(path: string, options?: SessionStorageWriterOpenOptions): StagedStreamingWriter;
304
418
  /**
305
419
  * Delete a session and sibling artifacts in an operator-selected explicit directory.
306
420
  * Default managed roots use deleteSessionVerified and never call this path.
@@ -317,10 +431,18 @@ export declare class MemorySessionStorage implements SessionStorage {
317
431
  #private;
318
432
  ensureDirSync(_dir: string): void;
319
433
  existsSync(path: string): boolean;
434
+ writeBytesOwnedSync(path: string, content: Buffer): void;
320
435
  writeTextSync(path: string, content: string): void;
321
436
  readTextSync(path: string): string;
322
437
  readBytesSync(path: string): Uint8Array;
323
438
  readSnapshotSync(path: string): SessionStorageSnapshot;
439
+ /**
440
+ * Bounded recorded-length read with descriptor identity validation: mirrors the
441
+ * file backend's contract (dev/ino/nlink identity, exact `length` bytes present)
442
+ * against the in-memory file model so parity tests can compare backends.
443
+ */
444
+ readRangeSync(path: string, start: number, length: number): SessionStorageRangeSnapshot;
445
+ readRange(path: string, start: number, length: number): Promise<SessionStorageRangeSnapshot>;
324
446
  statSync(path: string): SessionStorageStat;
325
447
  listFilesSync(dir: string, pattern: string): string[];
326
448
  listFilesByMtime(dir: string, pattern: string): Promise<Array<{
@@ -334,10 +456,58 @@ export declare class MemorySessionStorage implements SessionStorage {
334
456
  writeText(path: string, content: string): Promise<void>;
335
457
  rename(path: string, nextPath: string): Promise<void>;
336
458
  renameSync(path: string, nextPath: string): void;
459
+ replaceExactSync(sourcePath: string, destinationPath: string, expected: SessionStorageExactReplacementExpectation): boolean;
337
460
  unlink(path: string): Promise<void>;
338
461
  unlinkSync(path: string): void;
339
462
  deleteSessionWithArtifacts(sessionPath: string): Promise<void>;
340
463
  deleteSessionVerified(target: VerifiedSessionDeleteTarget): Promise<VerifiedSessionDeleteResult>;
341
464
  openWriter(path: string, options?: SessionStorageWriterOpenOptions): SessionStorageWriter;
465
+ openStagedWriter(path: string): StagedStreamingWriter;
342
466
  }
467
+ /**
468
+ * Outcome of a disk-retention retirement attempt.
469
+ *
470
+ * `kept` means the transcript survived and nothing of the session was
471
+ * destroyed, with the exact reason so `gjc gc --disk` can report it.
472
+ * `cleanup_pending` means the delete authority already detached or removed the
473
+ * session's artifact tree (or quarantined the transcript) before it stopped:
474
+ * the record survives, the session does not, and a caller must NOT report that
475
+ * as an ordinary keep.
476
+ */
477
+ export type SessionRetirementOutcome = {
478
+ kind: "retired";
479
+ } | {
480
+ kind: "kept";
481
+ reason: string;
482
+ } | {
483
+ kind: "cleanup_pending";
484
+ reason: string;
485
+ };
486
+ /**
487
+ * Non-mutating projection of {@link retireSessionTranscript}: would the
488
+ * retention pass's own preconditions let this transcript be retired at all?
489
+ *
490
+ * `gjc gc --disk` runs it so a dry run reports the verdict a prune would reach
491
+ * instead of promising bytes the delete authority will refuse to release. The
492
+ * authority's verdict (containment, identity, artifact tree) is deliberately
493
+ * not predicted here — it is re-derived against live state at delete time.
494
+ */
495
+ export declare function probeSessionRetirement(storage: FileSessionStorage, sessionsRoot: string, transcriptPath: string): {
496
+ kind: "retirable";
497
+ } | {
498
+ kind: "kept";
499
+ reason: string;
500
+ };
501
+ /**
502
+ * Retire one managed session transcript through the verified hard-delete
503
+ * authority.
504
+ *
505
+ * This is the only supported way for a retention pass to remove a transcript:
506
+ * identity, containment, header id/cwd, artifact tree and parent directory are
507
+ * all re-verified inside {@link FileSessionStorage.deleteSessionVerified}, and
508
+ * anything ambiguous fails closed as `kept` rather than deleting bytes. The
509
+ * caller owns the retention policy; this function owns nothing but the delete
510
+ * authority.
511
+ */
512
+ export declare function retireSessionTranscript(storage: FileSessionStorage, sessionsRoot: string, transcriptPath: string): Promise<SessionRetirementOutcome>;
343
513
  export {};
@@ -16,6 +16,7 @@ import type { ForkContextSeed } from "../session/agent-session";
16
16
  import type { ArtifactManager } from "../session/artifacts";
17
17
  import type { AuthStorage } from "../session/auth-storage";
18
18
  import { SessionManager } from "../session/session-manager";
19
+ import "../tools/yield";
19
20
  import type { ContextFileEntry } from "../tools";
20
21
  import type { EventBus } from "../utils/event-bus";
21
22
  import type { WorkspaceTree } from "../workspace-tree";
@@ -112,7 +113,7 @@ export interface ExecutorOptions {
112
113
  export declare class ManagedTaskPersistence {
113
114
  #private;
114
115
  constructor(artifacts: ArtifactManager, taskId: string);
115
- openSession(cwd: string): Promise<SessionManager>;
116
+ openSession(cwd: string, sessionMemoryMode?: "off" | "shadow" | "enabled"): Promise<SessionManager>;
116
117
  publishOutput(rawOutput: string, metadata: Uint8Array): Promise<void>;
117
118
  }
118
119
  export declare function createManagedTaskPersistence(artifacts: ArtifactManager, taskId: string): ManagedTaskPersistence;
@@ -118,6 +118,7 @@ export interface ShellRendererConfig<TArgs> {
118
118
  resolveCommand?: (args: TArgs | undefined) => string | undefined;
119
119
  resolveCwd?: (args: TArgs | undefined) => string | undefined;
120
120
  resolveEnv?: (args: TArgs | undefined) => Record<string, string> | undefined;
121
+ compactCommand?: boolean;
121
122
  }
122
123
  export declare function getBashEnvForDisplay(args: BashRenderArgs): Record<string, string> | undefined;
123
124
  export declare function formatBashCommand(args: BashRenderArgs): string;
@@ -11,6 +11,12 @@ export interface OutputBlockOptions {
11
11
  width: number;
12
12
  applyBg?: boolean;
13
13
  }
14
+ /**
15
+ * Return the width available to section content after the output block's
16
+ * visible left and right borders. Keep callers on the same layout contract as
17
+ * renderOutputBlock so width-aware projections are not wrapped a second time.
18
+ */
19
+ export declare function getOutputBlockContentWidth(width: number, theme: Theme): number;
14
20
  export declare function renderOutputBlock(options: OutputBlockOptions, theme: Theme): string[];
15
21
  /**
16
22
  * Cached wrapper around `renderOutputBlock`.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/coding-agent",
4
- "version": "0.12.18",
4
+ "version": "0.12.20",
5
5
  "description": "Gajae Code CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://gajae-code.com",
7
7
  "author": "Yeachan-Heo and Gajae Code Contributors",
@@ -54,13 +54,13 @@
54
54
  "@agentclientprotocol/sdk": "1.3.0",
55
55
  "@babel/parser": "^7.29.3",
56
56
  "@mozilla/readability": "^0.6.0",
57
- "@gajae-code/stats": "0.12.18",
58
- "@gajae-code/agent-core": "0.12.18",
59
- "@gajae-code/ai": "0.12.18",
60
- "@gajae-code/bridge-client": "0.12.18",
61
- "@gajae-code/natives": "0.12.18",
62
- "@gajae-code/tui": "0.12.18",
63
- "@gajae-code/utils": "0.12.18",
57
+ "@gajae-code/stats": "0.12.20",
58
+ "@gajae-code/agent-core": "0.12.20",
59
+ "@gajae-code/ai": "0.12.20",
60
+ "@gajae-code/bridge-client": "0.12.20",
61
+ "@gajae-code/natives": "0.12.20",
62
+ "@gajae-code/tui": "0.12.20",
63
+ "@gajae-code/utils": "0.12.20",
64
64
  "@puppeteer/browsers": "^2.13.0",
65
65
  "@types/turndown": "5.0.6",
66
66
  "@xterm/headless": "^6.0.0",
package/src/cli/args.ts CHANGED
@@ -80,6 +80,25 @@ function isStartupLoginCommandArg(args: readonly string[], index: number): boole
80
80
  return argumentCount === 0 || (argumentCount === 1 && !args[index + 1].startsWith("-"));
81
81
  }
82
82
 
83
+ function takeFlagValue(args: readonly string[], index: number, flag: string, allowDashPrefixed = false): string {
84
+ const value = args[index + 1];
85
+ if (!value || (!allowDashPrefixed && value.startsWith("-"))) {
86
+ throw new CliParseError(`${flag} requires a value`);
87
+ }
88
+ return value;
89
+ }
90
+
91
+ function takePromptValue(
92
+ args: readonly string[],
93
+ index: number,
94
+ flag: string,
95
+ allowInlineDashPrefixed: boolean,
96
+ ): string {
97
+ const value = args[index + 1];
98
+ const allowSeparatedDashPrefixed = value === "-" || value?.startsWith("- ") === true;
99
+ return takeFlagValue(args, index, flag, allowInlineDashPrefixed || allowSeparatedDashPrefixed);
100
+ }
101
+
83
102
  export function parseArgs(args: string[]): Args {
84
103
  const result: Args = {
85
104
  messages: [],
@@ -89,6 +108,7 @@ export function parseArgs(args: string[]): Args {
89
108
 
90
109
  for (let i = 0; i < args.length; i++) {
91
110
  let arg = args[i];
111
+ let hasInlineValue = false;
92
112
 
93
113
  if (isStartupLoginCommandArg(args, i)) {
94
114
  result.authBootstrap = true;
@@ -106,6 +126,7 @@ export function parseArgs(args: string[]): Args {
106
126
  const eqIdx = arg.indexOf("=");
107
127
  const value = arg.slice(eqIdx + 1);
108
128
  arg = arg.slice(0, eqIdx);
129
+ hasInlineValue = true;
109
130
  // Insert the value so the existing "args[++i]" logic picks it up
110
131
  args.splice(i + 1, 0, value);
111
132
  }
@@ -116,8 +137,9 @@ export function parseArgs(args: string[]): Args {
116
137
  result.version = true;
117
138
  } else if (arg === "--allow-home") {
118
139
  result.allowHome = true;
119
- } else if (arg === "--mode" && i + 1 < args.length) {
120
- const mode = args[++i];
140
+ } else if (arg === "--mode") {
141
+ const mode = takeFlagValue(args, i, "--mode");
142
+ i++;
121
143
  if (mode === "text" || mode === "json" || mode === "acp") {
122
144
  result.mode = mode;
123
145
  } else {
@@ -137,34 +159,34 @@ export function parseArgs(args: string[]): Args {
137
159
  } else {
138
160
  result.resume = true;
139
161
  }
140
- } else if (arg === "--fork" && i + 1 < args.length) {
141
- result.fork = args[++i];
142
- } else if (arg === "--provider" && i + 1 < args.length) {
143
- result.provider = args[++i];
144
- } else if (arg === "--model" && i + 1 < args.length) {
145
- result.model = args[++i];
146
- } else if (arg === "--smol" && i + 1 < args.length) {
147
- result.smol = args[++i];
148
- } else if (arg === "--slow" && i + 1 < args.length) {
149
- result.slow = args[++i];
150
- } else if (arg === "--plan" && i + 1 < args.length) {
151
- result.plan = args[++i];
152
- } else if (arg === "--mpreset" && i + 1 < args.length) {
153
- result.mpreset = args[++i];
162
+ } else if (arg === "--fork") {
163
+ result.fork = takeFlagValue(args, i++, "--fork");
164
+ } else if (arg === "--provider") {
165
+ result.provider = takeFlagValue(args, i++, "--provider");
166
+ } else if (arg === "--model") {
167
+ result.model = takeFlagValue(args, i++, "--model");
168
+ } else if (arg === "--smol") {
169
+ result.smol = takeFlagValue(args, i++, "--smol");
170
+ } else if (arg === "--slow") {
171
+ result.slow = takeFlagValue(args, i++, "--slow");
172
+ } else if (arg === "--plan") {
173
+ result.plan = takeFlagValue(args, i++, "--plan");
174
+ } else if (arg === "--mpreset") {
175
+ result.mpreset = takeFlagValue(args, i++, "--mpreset");
154
176
  } else if (arg === "--default") {
155
177
  result.default = true;
156
- } else if (arg === "--api-key" && i + 1 < args.length) {
157
- result.apiKey = args[++i];
178
+ } else if (arg === "--api-key") {
179
+ result.apiKey = takeFlagValue(args, i++, "--api-key");
158
180
  } else if (arg === "--credential") {
159
181
  const next = args[i + 1];
160
182
  if (!next || next.startsWith("-")) {
161
183
  throw new CliParseError("--credential requires <selector>");
162
184
  }
163
185
  result.credential = args[++i];
164
- } else if (arg === "--system-prompt" && i + 1 < args.length) {
165
- result.systemPrompt = args[++i];
166
- } else if (arg === "--append-system-prompt" && i + 1 < args.length) {
167
- result.appendSystemPrompt = args[++i];
186
+ } else if (arg === "--system-prompt") {
187
+ result.systemPrompt = takePromptValue(args, i++, "--system-prompt", hasInlineValue);
188
+ } else if (arg === "--append-system-prompt") {
189
+ result.appendSystemPrompt = takePromptValue(args, i++, "--append-system-prompt", hasInlineValue);
168
190
  } else if (arg === "--clipboard-transport") {
169
191
  const next = args[i + 1];
170
192
  if (!next || next.startsWith("-")) {
@@ -196,14 +218,16 @@ export function parseArgs(args: string[]): Args {
196
218
  throw new CliParseError("--mcp-config requires <absolute-path>");
197
219
  }
198
220
  result.mcpConfig = args[++i];
199
- } else if (arg === "--provider-session-id" && i + 1 < args.length) {
200
- result.providerSessionId = args[++i];
221
+ } else if (arg === "--provider-session-id") {
222
+ result.providerSessionId = takeFlagValue(args, i++, "--provider-session-id");
201
223
  } else if (arg === "--no-session") {
202
224
  result.noSession = true;
203
- } else if (arg === "--session-dir" && i + 1 < args.length) {
204
- result.sessionDir = args[++i];
205
- } else if (arg === "--models" && i + 1 < args.length) {
206
- result.models = args[++i].split(",").map(s => s.trim());
225
+ } else if (arg === "--session-dir") {
226
+ result.sessionDir = takeFlagValue(args, i++, "--session-dir");
227
+ } else if (arg === "--models") {
228
+ result.models = takeFlagValue(args, i++, "--models")
229
+ .split(",")
230
+ .map(s => s.trim());
207
231
  } else if (arg === "--no-tools") {
208
232
  result.noTools = true;
209
233
  } else if (arg === "--no-lsp") {
@@ -212,8 +236,23 @@ export function parseArgs(args: string[]): Args {
212
236
  result.noPty = true;
213
237
  } else if (arg === "--tmux") {
214
238
  result.tmux = true;
215
- } else if (arg === "--tools" && i + 1 < args.length) {
216
- const toolNames = args[++i]
239
+ } else if (arg === "--extension" || arg === "-e") {
240
+ const extension = takeFlagValue(args, i++, "--extension");
241
+ result.extensions = [...(result.extensions ?? []), extension];
242
+ } else if (arg === "--hook") {
243
+ const hook = takeFlagValue(args, i++, "--hook");
244
+ result.hooks = [...(result.hooks ?? []), hook];
245
+ } else if (arg === "--no-extensions") {
246
+ result.noExtensions = true;
247
+ } else if (arg === "--no-skills") {
248
+ result.noSkills = true;
249
+ } else if (arg === "--skills") {
250
+ result.skills = takeFlagValue(args, i++, "--skills")
251
+ .split(",")
252
+ .map(s => s.trim())
253
+ .filter(Boolean);
254
+ } else if (arg === "--tools") {
255
+ const toolNames = takeFlagValue(args, i++, "--tools")
217
256
  .split(",")
218
257
  .map(s => s.trim().toLowerCase())
219
258
  .filter(Boolean);
@@ -248,8 +287,8 @@ export function parseArgs(args: string[]): Args {
248
287
  result.thinking = thinking;
249
288
  } else if (arg === "--print" || arg === "-p") {
250
289
  result.print = true;
251
- } else if (arg === "--export" && i + 1 < args.length) {
252
- result.export = args[++i];
290
+ } else if (arg === "--export") {
291
+ result.export = takeFlagValue(args, i++, "--export");
253
292
  } else if (arg === "--no-rules") {
254
293
  result.noRules = true;
255
294
  } else if (arg === "--no-title") {
@@ -263,13 +302,15 @@ export function parseArgs(args: string[]): Args {
263
302
  }
264
303
  } else if (arg.startsWith("@")) {
265
304
  result.fileArgs.push(arg.slice(1)); // Remove @ prefix
266
- } else if (!arg.startsWith("-")) {
305
+ } else if (arg.startsWith("-")) {
306
+ result.unknownFlags.set(arg, true);
307
+ } else {
267
308
  result.messages.push(arg);
268
309
  }
269
310
  }
270
311
 
271
312
  if (result.default && !result.mpreset) {
272
- throw new Error("--default requires --mpreset <name>");
313
+ throw new CliParseError("--default requires --mpreset <name>");
273
314
  }
274
315
  if (
275
316
  result.mcpConfig !== undefined &&
@@ -4,7 +4,7 @@
4
4
  * Thin wrapper around the launch flow that forces `mode: "acp"` unless the
5
5
  * ACP terminal-auth flag asks the same command to open the interactive TUI.
6
6
  */
7
- import { Command } from "@gajae-code/utils/cli";
7
+ import { CliParseError, Command } from "@gajae-code/utils/cli";
8
8
  import { parseArgs } from "../cli/args";
9
9
  import { runRootCommand } from "../main";
10
10
  import { prepareAcpTerminalAuthArgs } from "../modes/acp/terminal-auth";
@@ -16,6 +16,12 @@ export default class Acp extends Command {
16
16
  async run(): Promise<void> {
17
17
  const { args, terminalAuth } = prepareAcpTerminalAuthArgs(this.argv);
18
18
  const parsed = parseArgs(args);
19
+ if (parsed.unknownFlags.size > 0) {
20
+ throw new CliParseError(`Unknown ACP option: ${[...parsed.unknownFlags.keys()].join(", ")}`);
21
+ }
22
+ if (terminalAuth && parsed.mode !== undefined) {
23
+ throw new CliParseError("--acp-terminal-auth only supports --mode acp");
24
+ }
19
25
  if (!terminalAuth) {
20
26
  parsed.mode = "acp";
21
27
  }
@@ -1,5 +1,21 @@
1
1
  import { Command, Flags } from "@gajae-code/utils/cli";
2
- import { runGjcGcCommand } from "../gjc-runtime/gc-runtime";
2
+ import { isSettingsInitialized, settings } from "../config/settings";
3
+ import { type GcDiskPolicy, runGjcGcCommand } from "../gjc-runtime/gc-runtime";
4
+
5
+ /**
6
+ * Resolve the `gc.*` retention knobs from settings. When settings are not
7
+ * initialized (headless/early invocation) the runtime falls back to its
8
+ * schema-backed defaults, so this returns nothing rather than guessing.
9
+ */
10
+ function resolveDiskPolicyFromSettings(): Partial<GcDiskPolicy> | undefined {
11
+ if (!isSettingsInitialized()) return undefined;
12
+ return {
13
+ sessions_max_age_days: settings.get("gc.sessions.maxAgeDays"),
14
+ sessions_max_total_bytes: settings.get("gc.sessions.maxTotalBytes"),
15
+ natives_keep_versions: settings.get("gc.natives.keepVersions"),
16
+ backups_max_age_days: settings.get("gc.backups.maxAgeDays"),
17
+ };
18
+ }
3
19
 
4
20
  export default class Gc extends Command {
5
21
  static description = "Garbage-collect stale GJC session/PID records (dry-run by default)";
@@ -9,6 +25,10 @@ export default class Gc extends Command {
9
25
  prune: Flags.boolean({ description: "Remove stale records (default: report only)", default: false }),
10
26
  force: Flags.boolean({ description: "Alias for --prune (eligible records only)", default: false }),
11
27
  "dry-run": Flags.boolean({ description: "Force report-only mode", default: false }),
28
+ disk: Flags.boolean({
29
+ description: "Also report on-disk retention (sessions, blobs, natives, backups)",
30
+ default: false,
31
+ }),
12
32
  "repair-session-index": Flags.boolean({
13
33
  description: "Quarantine a corrupt session-index suffix and retain its valid prefix",
14
34
  default: false,
@@ -20,11 +40,20 @@ export default class Gc extends Command {
20
40
  "gjc gc --json",
21
41
  "gjc gc --prune",
22
42
  "gjc gc --prune --json",
43
+ "gjc gc --disk",
44
+ "gjc gc --disk --json",
45
+ "gjc gc --disk --prune",
23
46
  "gjc gc --repair-session-index --json",
24
47
  ];
25
48
 
26
49
  async run(): Promise<void> {
27
- const result = await runGjcGcCommand(this.argv, process.cwd(), process.env);
50
+ const result = await runGjcGcCommand(
51
+ this.argv,
52
+ process.cwd(),
53
+ process.env,
54
+ undefined,
55
+ resolveDiskPolicyFromSettings(),
56
+ );
28
57
  if (result.stdout) process.stdout.write(result.stdout);
29
58
  if (result.stderr) process.stderr.write(result.stderr);
30
59
  process.exitCode = result.status;