@kenkaiiii/ggcoder 5.68.1 → 5.69.0

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 (76) hide show
  1. package/dist/app-sidecar.js +26 -11
  2. package/dist/app-sidecar.js.map +1 -1
  3. package/dist/cli.js +15 -14
  4. package/dist/cli.js.map +1 -1
  5. package/dist/core/agent-session.d.ts +17 -0
  6. package/dist/core/agent-session.js +77 -0
  7. package/dist/core/agent-session.js.map +1 -1
  8. package/dist/core/cache-diagnostics.d.ts +6 -0
  9. package/dist/core/cache-diagnostics.js +31 -0
  10. package/dist/core/cache-diagnostics.js.map +1 -1
  11. package/dist/core/cache-expiry.d.ts +87 -0
  12. package/dist/core/cache-expiry.js +111 -0
  13. package/dist/core/cache-expiry.js.map +1 -0
  14. package/dist/core/destructive-git-guard.d.ts +27 -0
  15. package/dist/core/destructive-git-guard.js +94 -54
  16. package/dist/core/destructive-git-guard.js.map +1 -1
  17. package/dist/core/injection-detect.d.ts +38 -0
  18. package/dist/core/injection-detect.js +232 -0
  19. package/dist/core/injection-detect.js.map +1 -0
  20. package/dist/core/mcp/client.d.ts +72 -0
  21. package/dist/core/mcp/client.js +264 -41
  22. package/dist/core/mcp/client.js.map +1 -1
  23. package/dist/core/mcp/store.d.ts +6 -1
  24. package/dist/core/mcp/store.js +12 -1
  25. package/dist/core/mcp/store.js.map +1 -1
  26. package/dist/core/mcp/types.d.ts +18 -0
  27. package/dist/core/persistent-shell.d.ts +58 -6
  28. package/dist/core/persistent-shell.js +331 -49
  29. package/dist/core/persistent-shell.js.map +1 -1
  30. package/dist/core/progress/git-xp.js +8 -14
  31. package/dist/core/progress/git-xp.js.map +1 -1
  32. package/dist/core/subagent-manager.d.ts +9 -0
  33. package/dist/core/subagent-manager.js +7 -1
  34. package/dist/core/subagent-manager.js.map +1 -1
  35. package/dist/core/subagent-receipt.d.ts +54 -0
  36. package/dist/core/subagent-receipt.js +276 -0
  37. package/dist/core/subagent-receipt.js.map +1 -0
  38. package/dist/core/subagent-turn-record.d.ts +2 -0
  39. package/dist/core/subagent-turn-record.js.map +1 -1
  40. package/dist/core/verification-snapshot.js +3 -5
  41. package/dist/core/verification-snapshot.js.map +1 -1
  42. package/dist/core/workspace-guard.d.ts +18 -7
  43. package/dist/core/workspace-guard.js +227 -60
  44. package/dist/core/workspace-guard.js.map +1 -1
  45. package/dist/modes/subagent-worker-mode.js +18 -2
  46. package/dist/modes/subagent-worker-mode.js.map +1 -1
  47. package/dist/tools/bash.js +12 -4
  48. package/dist/tools/bash.js.map +1 -1
  49. package/dist/tools/read.js +3 -2
  50. package/dist/tools/read.js.map +1 -1
  51. package/dist/tools/steroids.d.ts +1 -1
  52. package/dist/tools/subagent-control.js +34 -6
  53. package/dist/tools/subagent-control.js.map +1 -1
  54. package/dist/tools/subagent-shared.d.ts +13 -0
  55. package/dist/tools/subagent-shared.js +25 -0
  56. package/dist/tools/subagent-shared.js.map +1 -1
  57. package/dist/tools/subagent.js +14 -3
  58. package/dist/tools/subagent.js.map +1 -1
  59. package/dist/tools/task-output.js +3 -2
  60. package/dist/tools/task-output.js.map +1 -1
  61. package/dist/tools/truncate.d.ts +21 -0
  62. package/dist/tools/truncate.js +187 -0
  63. package/dist/tools/truncate.js.map +1 -1
  64. package/dist/tools/ui-adopt.js +2 -0
  65. package/dist/tools/ui-adopt.js.map +1 -1
  66. package/dist/utils/git.d.ts +77 -0
  67. package/dist/utils/git.js +285 -21
  68. package/dist/utils/git.js.map +1 -1
  69. package/dist/utils/github-ci.js +2 -1
  70. package/dist/utils/github-ci.js.map +1 -1
  71. package/dist/utils/github.js +11 -9
  72. package/dist/utils/github.js.map +1 -1
  73. package/dist/utils/process.d.ts +20 -0
  74. package/dist/utils/process.js +98 -0
  75. package/dist/utils/process.js.map +1 -1
  76. package/package.json +4 -4
@@ -1,4 +1,5 @@
1
1
  import type { CacheRetention, PreparedContext, Usage } from "@kenkaiiii/gg-ai";
2
+ import { type CacheTouch } from "./cache-expiry.js";
2
3
  export interface CacheRequestObservation {
3
4
  provider: string;
4
5
  model: string;
@@ -50,6 +51,11 @@ export declare class CacheDiagnostics {
50
51
  private freedTokens;
51
52
  private requests;
52
53
  private reportedCache;
54
+ private lastTouch;
55
+ /** Last successful cache-using request (real turn or prewarm), or null. */
56
+ lastCacheTouch(): CacheTouch | null;
57
+ /** Record a successful cache-using request that bypassed prepare/complete (prewarm). */
58
+ noteCacheTouch(touch: CacheTouch): void;
53
59
  reset(): void;
54
60
  discardAttempt(): void;
55
61
  noteEdit(edit: HistoryEdit, freedTokens?: number): void;
@@ -1,4 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { resolveCacheTtl } from "./cache-expiry.js";
2
3
  /** Digests never leave this instance. Canonical object keys avoid spurious edits on resume. */
3
4
  function digest(value) {
4
5
  const json = JSON.stringify(value, (_key, item) => item !== null && typeof item === "object" && !Array.isArray(item)
@@ -8,6 +9,13 @@ function digest(value) {
8
9
  .update(json ?? "null")
9
10
  .digest("hex");
10
11
  }
12
+ /** Route fields the session passes as `route`; read structurally, never digested. */
13
+ function routeField(route, key) {
14
+ if (!route || typeof route !== "object")
15
+ return undefined;
16
+ const value = route[key];
17
+ return typeof value === "string" && value ? value : undefined;
18
+ }
11
19
  function signedThinkingCount(message) {
12
20
  if (message.role !== "assistant" || !Array.isArray(message.content))
13
21
  return 0;
@@ -28,7 +36,17 @@ export class CacheDiagnostics {
28
36
  freedTokens = 0;
29
37
  requests = 0;
30
38
  reportedCache = false;
39
+ lastTouch = null;
40
+ /** Last successful cache-using request (real turn or prewarm), or null. */
41
+ lastCacheTouch() {
42
+ return this.lastTouch;
43
+ }
44
+ /** Record a successful cache-using request that bypassed prepare/complete (prewarm). */
45
+ noteCacheTouch(touch) {
46
+ this.lastTouch = { ...touch };
47
+ }
31
48
  reset() {
49
+ this.lastTouch = null;
32
50
  this.completed = undefined;
33
51
  this.pending = undefined;
34
52
  this.edits.clear();
@@ -56,6 +74,16 @@ export class CacheDiagnostics {
56
74
  route: digest({ provider: input.provider, model: input.model, route: input.route }),
57
75
  imagesDropped: context.imagesBefore - context.imagesAfter,
58
76
  at: input.at,
77
+ touch: (() => {
78
+ const policy = resolveCacheTtl({
79
+ provider: input.provider,
80
+ model: input.model,
81
+ cacheRetention: input.cacheRetention,
82
+ baseUrl: routeField(input.route, "baseUrl"),
83
+ accountId: routeField(input.route, "accountId"),
84
+ });
85
+ return policy ? { provider: input.provider, model: input.model, policy } : null;
86
+ })(),
59
87
  };
60
88
  let unchanged = 0;
61
89
  if (previous) {
@@ -143,6 +171,9 @@ export class CacheDiagnostics {
143
171
  ? Math.max(0, Math.min(promptTokens(previous.usage), promptTokens(usage)) - cacheRead)
144
172
  : null;
145
173
  this.completed = { ...snapshot, usage: { ...usage } };
174
+ // The provider refreshes the cached prefix when the request is processed, so
175
+ // the TTL clock restarts at the request's start time.
176
+ this.lastTouch = snapshot.touch ? { ...snapshot.touch, at: snapshot.at } : null;
146
177
  this.pending = undefined;
147
178
  this.edits.clear();
148
179
  this.freedTokens = 0;
@@ -1 +1 @@
1
- {"version":3,"file":"cache-diagnostics.js","sourceRoot":"","sources":["../../src/core/cache-diagnostics.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cache-diagnostics.js","sourceRoot":"","sources":["../../src/core/cache-diagnostics.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,87 @@
1
+ import type { CacheRetention } from "@kenkaiiii/gg-ai";
2
+ /**
3
+ * Cold-prompt-cache detection (mirrors langchain-ai/deepagents#6477's
4
+ * `cold_cache.py`): providers keep the conversation prefix cached only for a
5
+ * TTL, so after a break the next message re-reads the whole context uncached.
6
+ *
7
+ * Every TTL below is derived from what gg-ai actually puts on the wire — never
8
+ * from a provider's marketing maximum we did not request. A provider/route we
9
+ * cannot pin to a request-side TTL returns `null` (no warning) rather than a
10
+ * guess.
11
+ */
12
+ export interface CacheTtlPolicy {
13
+ ttlMs: number;
14
+ /**
15
+ * `expired`: the TTL is a documented/requested *maximum* — past it the entry
16
+ * is gone. `may_be_cold`: the TTL is a guaranteed *minimum* (GPT-5.6+ `30m`),
17
+ * so past it the prefix may still be warm.
18
+ */
19
+ confidence: "expired" | "may_be_cold";
20
+ /** Where the number comes from, for logs/diagnostics. */
21
+ source: string;
22
+ }
23
+ export interface CacheTtlInput {
24
+ provider: string;
25
+ model: string;
26
+ cacheRetention: CacheRetention;
27
+ baseUrl?: string;
28
+ /** OpenAI OAuth (ChatGPT/Codex) routes carry an account id. */
29
+ accountId?: string;
30
+ }
31
+ /** Resolve the cache TTL our provider code requests for this route. */
32
+ export declare function resolveCacheTtl(input: CacheTtlInput): CacheTtlPolicy | null;
33
+ /**
34
+ * Prefix size below which the notice is not worth an interruption. At 40k
35
+ * tokens a cold Anthropic re-read (1.25x write vs 0.1x cached read) costs ~12x
36
+ * the warm request, roughly a dime on Sonnet-class pricing; below that the delta
37
+ * is pennies — and compacting first itself re-reads the whole context once, so
38
+ * on small chats "Compact first" saves nothing.
39
+ */
40
+ export declare const CACHE_EXPIRY_NOTICE_MIN_TOKENS = 40000;
41
+ export interface CacheTouch {
42
+ at: number;
43
+ provider: string;
44
+ model: string;
45
+ policy: CacheTtlPolicy;
46
+ }
47
+ export interface CacheExpiryStatus {
48
+ /** Owning session, so clients can key "dismissed once per expiry per chat". */
49
+ sessionId?: string;
50
+ provider: string;
51
+ ttlMs: number;
52
+ confidence: CacheTtlPolicy["confidence"];
53
+ ttlSource: string;
54
+ /** Last successful cache-using request (real turn or prewarm), epoch ms. */
55
+ lastRequestAt: number | null;
56
+ /** When the cached prefix lapses/lapsed, epoch ms; null when age is unknown. */
57
+ expiresAt: number | null;
58
+ expired: boolean;
59
+ /**
60
+ * `idle`: TTL passed. `age_unknown`: resumed/reset session with no request in
61
+ * this process. `identity_changed`: provider/model switched since the last
62
+ * request, so the old prefix cannot be reused.
63
+ */
64
+ reason: "idle" | "age_unknown" | "identity_changed" | null;
65
+ /** Tokens the next request would re-read (the current context). */
66
+ prefixTokens: number;
67
+ minTokens: number;
68
+ /** expired AND prefixTokens >= minTokens: worth showing the notice. */
69
+ notable: boolean;
70
+ /** No authoritative pricing exists in GG (turn metric cost is "unavailable"). */
71
+ estimatedExtraCostUsd: null;
72
+ }
73
+ export interface AssessCacheExpiryInput {
74
+ /** Policy for the route the NEXT request will take. */
75
+ current: {
76
+ provider: string;
77
+ model: string;
78
+ policy: CacheTtlPolicy | null;
79
+ };
80
+ lastTouch: CacheTouch | null;
81
+ now: number;
82
+ prefixTokens: number;
83
+ /** False for an empty chat — nothing to re-read. */
84
+ hasHistory: boolean;
85
+ minTokens?: number;
86
+ }
87
+ export declare function assessCacheExpiry(input: AssessCacheExpiryInput): CacheExpiryStatus | null;
@@ -0,0 +1,111 @@
1
+ const MINUTE = 60_000;
2
+ /** Resolve the cache TTL our provider code requests for this route. */
3
+ export function resolveCacheTtl(input) {
4
+ if (input.cacheRetention === "none")
5
+ return null;
6
+ if (input.provider === "anthropic") {
7
+ // gg-ai/src/providers/transform.ts `toAnthropicCacheControl`: "long" sends
8
+ // `ttl: "1h"` only on the first-party endpoint; everything else sends a bare
9
+ // `{type: "ephemeral"}`, which is Anthropic's 5-minute default.
10
+ const firstParty = !input.baseUrl || input.baseUrl.includes("api.anthropic.com");
11
+ if (input.cacheRetention === "long" && firstParty) {
12
+ return {
13
+ ttlMs: 60 * MINUTE,
14
+ confidence: "expired",
15
+ source: "anthropic cache_control ttl=1h (transform.ts toAnthropicCacheControl)",
16
+ };
17
+ }
18
+ return {
19
+ ttlMs: 5 * MINUTE,
20
+ confidence: "expired",
21
+ source: "anthropic cache_control ephemeral default 5m (transform.ts toAnthropicCacheControl)",
22
+ };
23
+ }
24
+ if (input.provider === "openai") {
25
+ if (input.accountId) {
26
+ // gg-ai/src/providers/openai-codex.ts: the Codex backend rejects
27
+ // prompt_cache_retention; "Cache TTL on Codex is controlled server-side
28
+ // (~5-10 min in-memory)". Use the upper bound so we only warn once the
29
+ // entry is surely gone.
30
+ return {
31
+ ttlMs: 10 * MINUTE,
32
+ confidence: "expired",
33
+ source: "codex server-side in-memory cache ~5-10m (openai-codex.ts)",
34
+ };
35
+ }
36
+ if (input.model.startsWith("gpt-5.6") || input.model.startsWith("gpt-6")) {
37
+ // gg-ai/src/providers/openai.ts sends prompt_cache_options.ttl = "30m";
38
+ // OpenAI documents 30 minutes as a guaranteed minimum.
39
+ return {
40
+ ttlMs: 30 * MINUTE,
41
+ confidence: "may_be_cold",
42
+ source: "openai prompt_cache_options ttl=30m (openai.ts)",
43
+ };
44
+ }
45
+ if (input.cacheRetention === "long") {
46
+ // gg-ai/src/providers/openai.ts sends prompt_cache_retention = "24h".
47
+ return {
48
+ ttlMs: 24 * 60 * MINUTE,
49
+ confidence: "expired",
50
+ source: "openai prompt_cache_retention=24h (openai.ts)",
51
+ };
52
+ }
53
+ // No retention param sent → OpenAI's default in-memory retention, which
54
+ // its prompt-caching guide documents as up to one hour (a maximum).
55
+ return {
56
+ ttlMs: 60 * MINUTE,
57
+ confidence: "expired",
58
+ source: "openai default in_memory retention, max 1h (no param sent by openai.ts)",
59
+ };
60
+ }
61
+ // Other providers (Gemini, GLM, Moonshot, OpenRouter, local, …): we request
62
+ // no TTL and have no documented window to anchor a warning on.
63
+ return null;
64
+ }
65
+ /**
66
+ * Prefix size below which the notice is not worth an interruption. At 40k
67
+ * tokens a cold Anthropic re-read (1.25x write vs 0.1x cached read) costs ~12x
68
+ * the warm request, roughly a dime on Sonnet-class pricing; below that the delta
69
+ * is pennies — and compacting first itself re-reads the whole context once, so
70
+ * on small chats "Compact first" saves nothing.
71
+ */
72
+ export const CACHE_EXPIRY_NOTICE_MIN_TOKENS = 40_000;
73
+ export function assessCacheExpiry(input) {
74
+ const { current, lastTouch, now } = input;
75
+ if (!input.hasHistory || !current.policy)
76
+ return null;
77
+ const minTokens = input.minTokens ?? CACHE_EXPIRY_NOTICE_MIN_TOKENS;
78
+ const prefixTokens = Math.max(0, Math.round(input.prefixTokens));
79
+ let reason = null;
80
+ let expiresAt = null;
81
+ // The entry lives by the TTL it was WRITTEN with, not the current setting.
82
+ let policy = current.policy;
83
+ if (!lastTouch) {
84
+ reason = "age_unknown";
85
+ }
86
+ else if (lastTouch.provider !== current.provider || lastTouch.model !== current.model) {
87
+ reason = "identity_changed";
88
+ }
89
+ else {
90
+ policy = lastTouch.policy;
91
+ expiresAt = lastTouch.at + policy.ttlMs;
92
+ if (now >= expiresAt)
93
+ reason = "idle";
94
+ }
95
+ const expired = reason !== null;
96
+ return {
97
+ provider: current.provider,
98
+ ttlMs: policy.ttlMs,
99
+ confidence: policy.confidence,
100
+ ttlSource: policy.source,
101
+ lastRequestAt: lastTouch?.at ?? null,
102
+ expiresAt,
103
+ expired,
104
+ reason,
105
+ prefixTokens,
106
+ minTokens,
107
+ notable: expired && prefixTokens >= minTokens,
108
+ estimatedExtraCostUsd: null,
109
+ };
110
+ }
111
+ //# sourceMappingURL=cache-expiry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-expiry.js","sourceRoot":"","sources":["../../src/core/cache-expiry.ts"],"names":[],"mappings":"AAkCA,MAAM,MAAM,GAAG,MAAM,CAAC;AAEtB,uEAAuE;AACvE,MAAM,UAAU,eAAe,CAAC,KAAoB;IAClD,IAAI,KAAK,CAAC,cAAc,KAAK,MAAM;QAAE,OAAO,IAAI,CAAC;IAEjD,IAAI,KAAK,CAAC,QAAQ,KAAK,WAAW,EAAE,CAAC;QACnC,2EAA2E;QAC3E,6EAA6E;QAC7E,gEAAgE;QAChE,MAAM,UAAU,GAAG,CAAC,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAC,CAAC;QACjF,IAAI,KAAK,CAAC,cAAc,KAAK,MAAM,IAAI,UAAU,EAAE,CAAC;YAClD,OAAO;gBACL,KAAK,EAAE,EAAE,GAAG,MAAM;gBAClB,UAAU,EAAE,SAAS;gBACrB,MAAM,EAAE,uEAAuE;aAChF,CAAC;QACJ,CAAC;QACD,OAAO;YACL,KAAK,EAAE,CAAC,GAAG,MAAM;YACjB,UAAU,EAAE,SAAS;YACrB,MAAM,EAAE,qFAAqF;SAC9F,CAAC;IACJ,CAAC;IAED,IAAI,KAAK,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAChC,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;YACpB,iEAAiE;YACjE,wEAAwE;YACxE,uEAAuE;YACvE,wBAAwB;YACxB,OAAO;gBACL,KAAK,EAAE,EAAE,GAAG,MAAM;gBAClB,UAAU,EAAE,SAAS;gBACrB,MAAM,EAAE,4DAA4D;aACrE,CAAC;QACJ,CAAC;QACD,IAAI,KAAK,CAAC,KAAK,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;YACzE,wEAAwE;YACxE,uDAAuD;YACvD,OAAO;gBACL,KAAK,EAAE,EAAE,GAAG,MAAM;gBAClB,UAAU,EAAE,aAAa;gBACzB,MAAM,EAAE,iDAAiD;aAC1D,CAAC;QACJ,CAAC;QACD,IAAI,KAAK,CAAC,cAAc,KAAK,MAAM,EAAE,CAAC;YACpC,sEAAsE;YACtE,OAAO;gBACL,KAAK,EAAE,EAAE,GAAG,EAAE,GAAG,MAAM;gBACvB,UAAU,EAAE,SAAS;gBACrB,MAAM,EAAE,+CAA+C;aACxD,CAAC;QACJ,CAAC;QACD,wEAAwE;QACxE,oEAAoE;QACpE,OAAO;YACL,KAAK,EAAE,EAAE,GAAG,MAAM;YAClB,UAAU,EAAE,SAAS;YACrB,MAAM,EAAE,yEAAyE;SAClF,CAAC;IACJ,CAAC;IAED,4EAA4E;IAC5E,+DAA+D;IAC/D,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,8BAA8B,GAAG,MAAM,CAAC;AA+CrD,MAAM,UAAU,iBAAiB,CAAC,KAA6B;IAC7D,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,GAAG,EAAE,GAAG,KAAK,CAAC;IAC1C,IAAI,CAAC,KAAK,CAAC,UAAU,IAAI,CAAC,OAAO,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IACtD,MAAM,SAAS,GAAG,KAAK,CAAC,SAAS,IAAI,8BAA8B,CAAC;IACpE,MAAM,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC;IAEjE,IAAI,MAAM,GAAgC,IAAI,CAAC;IAC/C,IAAI,SAAS,GAAkB,IAAI,CAAC;IACpC,2EAA2E;IAC3E,IAAI,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAC5B,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,MAAM,GAAG,aAAa,CAAC;IACzB,CAAC;SAAM,IAAI,SAAS,CAAC,QAAQ,KAAK,OAAO,CAAC,QAAQ,IAAI,SAAS,CAAC,KAAK,KAAK,OAAO,CAAC,KAAK,EAAE,CAAC;QACxF,MAAM,GAAG,kBAAkB,CAAC;IAC9B,CAAC;SAAM,CAAC;QACN,MAAM,GAAG,SAAS,CAAC,MAAM,CAAC;QAC1B,SAAS,GAAG,SAAS,CAAC,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC;QACxC,IAAI,GAAG,IAAI,SAAS;YAAE,MAAM,GAAG,MAAM,CAAC;IACxC,CAAC;IACD,MAAM,OAAO,GAAG,MAAM,KAAK,IAAI,CAAC;IAChC,OAAO;QACL,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,UAAU,EAAE,MAAM,CAAC,UAAU;QAC7B,SAAS,EAAE,MAAM,CAAC,MAAM;QACxB,aAAa,EAAE,SAAS,EAAE,EAAE,IAAI,IAAI;QACpC,SAAS;QACT,OAAO;QACP,MAAM;QACN,YAAY;QACZ,SAAS;QACT,OAAO,EAAE,OAAO,IAAI,YAAY,IAAI,SAAS;QAC7C,qBAAqB,EAAE,IAAI;KAC5B,CAAC;AACJ,CAAC"}
@@ -39,6 +39,33 @@ export interface DestructiveGitMatch {
39
39
  /** branch -D targets; stash drop target. */
40
40
  refs: string[];
41
41
  }
42
+ /** Basename of a command word, `.exe` stripped (`/usr/bin/git`, `git.exe`). */
43
+ export declare function commandName(word: string): string;
44
+ /**
45
+ * Expand the home and working-directory forms a model writes in paths:
46
+ * `~`, `$HOME`/`${HOME}` (any case, which also covers PowerShell `$home`),
47
+ * `$env:USERPROFILE`/`$env:HOME`, cmd `%USERPROFILE%`, and `$PWD`/`${PWD}`. Other variables stay
48
+ * literal, so they resolve to a harmless relative path.
49
+ */
50
+ export declare function expandShellPath(p: string, cwd: string): string;
51
+ /** One simple command found in a shell string, after unwrapping. */
52
+ export interface ShellInvocation {
53
+ /** {@link commandName} of the command word. */
54
+ name: string;
55
+ args: readonly string[];
56
+ /** Directory it would run in after any earlier `cd` in the same script. */
57
+ cwd: string;
58
+ /** Run by `xargs`, which appends operands read from stdin. */
59
+ viaXargs: boolean;
60
+ }
61
+ /**
62
+ * Every simple command in `command` whose name satisfies `wanted`, in source
63
+ * order, after splitting on `&&`/`;`/`||`/pipes, entering subshells,
64
+ * substitutions and `if`/`for`/`while` bodies, unwrapping `bash -c`/`eval`/
65
+ * `pwsh -c` scripts and exec wrappers (`sudo`, `env`, `timeout`, `xargs`, …),
66
+ * and following `cd`. Pure: nothing is executed.
67
+ */
68
+ export declare function walkShell(command: string, cwd: string, wanted: (name: string) => boolean): ShellInvocation[];
42
69
  /**
43
70
  * Parse `command` and return every git invocation in it that can destroy
44
71
  * work, with the directory each would run in. Pure apart from `existsSync`
@@ -1,7 +1,7 @@
1
- import { execFile } from "node:child_process";
2
1
  import { existsSync } from "node:fs";
3
2
  import os from "node:os";
4
3
  import path from "node:path";
4
+ import { BackgroundGitError, runBackgroundGit } from "../utils/git.js";
5
5
  import { log } from "./logger.js";
6
6
  const MAX_DEPTH = 6;
7
7
  const GIT_TIMEOUT_MS = 3_000;
@@ -326,22 +326,34 @@ const SHELL_KEYWORDS = new Set([
326
326
  "function",
327
327
  ]);
328
328
  /** Basename of a command word, `.exe` stripped (`/usr/bin/git`, `git.exe`). */
329
- function commandName(word) {
329
+ export function commandName(word) {
330
330
  return (word.split(/[\\/]/).pop() ?? word).replace(/\.exe$/i, "");
331
331
  }
332
- function expandHome(p) {
333
- return p.replace(/^~(?=[\\/]|$)/, os.homedir());
332
+ /**
333
+ * Expand the home and working-directory forms a model writes in paths:
334
+ * `~`, `$HOME`/`${HOME}` (any case, which also covers PowerShell `$home`),
335
+ * `$env:USERPROFILE`/`$env:HOME`, cmd `%USERPROFILE%`, and `$PWD`/`${PWD}`. Other variables stay
336
+ * literal, so they resolve to a harmless relative path.
337
+ */
338
+ export function expandShellPath(p, cwd) {
339
+ return p
340
+ .replace(/^(?:~|\$\{?home\}?|\$env:(?:userprofile|home)|%userprofile%)(?=[\\/]|$)/i, os.homedir())
341
+ .replace(/^\$\{?PWD\}?(?=[\\/]|$)/, cwd);
342
+ }
343
+ function isPowerShell(name) {
344
+ const lower = name.toLowerCase();
345
+ return lower === "powershell" || lower === "pwsh";
334
346
  }
335
- function analyzeScript(script, ctx, out) {
347
+ function walkScript(script, ctx) {
336
348
  const commands = [];
337
349
  scanShell(script, ctx.depth, commands);
338
350
  // `cd` persists for later commands in the same script (approximation: a
339
351
  // subshell's cd leaks too, which only ever widens what gets checked).
340
- const local = { cwd: ctx.cwd, depth: ctx.depth };
352
+ const local = { ...ctx };
341
353
  for (const words of commands)
342
- analyzeWords(words, local, out);
354
+ walkWords(words, local);
343
355
  }
344
- function analyzeWords(input, ctx, out) {
356
+ function walkWords(input, ctx) {
345
357
  let words = input;
346
358
  while (words.length > 0 &&
347
359
  (SHELL_KEYWORDS.has(words[0] ?? "") || /^[A-Za-z_][A-Za-z0-9_]*=/.test(words[0] ?? ""))) {
@@ -354,31 +366,38 @@ function analyzeWords(input, ctx, out) {
354
366
  const args = words.slice(1);
355
367
  if (name === "cd" || name === "pushd") {
356
368
  const target = args.find((a) => !a.startsWith("-"));
357
- if (target === undefined)
369
+ // `cd -` goes somewhere we cannot know; keep the current guess.
370
+ if (target === undefined && !args.includes("-"))
358
371
  ctx.cwd = os.homedir();
359
- else
360
- ctx.cwd = path.resolve(ctx.cwd, expandHome(target));
372
+ else if (target !== undefined)
373
+ ctx.cwd = path.resolve(ctx.cwd, expandShellPath(target, ctx.cwd));
361
374
  return;
362
375
  }
363
- if (name === "git") {
364
- const match = analyzeGit(args, ctx.cwd);
365
- if (match)
366
- out.push(match);
376
+ if (ctx.wanted(name)) {
377
+ ctx.out.push({ name, args, cwd: ctx.cwd, viaXargs: ctx.viaXargs });
367
378
  return;
368
379
  }
369
380
  if (ctx.depth >= MAX_DEPTH)
370
381
  return;
371
- const deeper = { cwd: ctx.cwd, depth: ctx.depth + 1 };
382
+ const deeper = { ...ctx, depth: ctx.depth + 1 };
372
383
  if (SHELLS.has(name)) {
373
384
  // `bash -c 'script'`, also clustered (`-lc`, `-ec`).
374
385
  const flagIndex = args.findIndex((a) => /^-[A-Za-z]*c[A-Za-z]*$/.test(a));
375
386
  const script = flagIndex === -1 ? undefined : args[flagIndex + 1];
376
387
  if (script !== undefined)
377
- analyzeScript(script, deeper, out);
388
+ walkScript(script, deeper);
389
+ return;
390
+ }
391
+ if (isPowerShell(name)) {
392
+ // `pwsh -c '…'` / `powershell -Command "…"` (any unambiguous prefix).
393
+ const flagIndex = args.findIndex((a) => /^-c(?:o(?:m(?:m(?:a(?:n(?:d)?)?)?)?)?)?$/i.test(a));
394
+ const script = flagIndex === -1 ? undefined : args.slice(flagIndex + 1).join(" ");
395
+ if (script)
396
+ walkScript(script, deeper);
378
397
  return;
379
398
  }
380
399
  if (name === "eval") {
381
- analyzeScript(args.join(" "), deeper, out);
400
+ walkScript(args.join(" "), deeper);
382
401
  return;
383
402
  }
384
403
  if (EXEC_WRAPPERS.has(name)) {
@@ -387,15 +406,13 @@ function analyzeWords(input, ctx, out) {
387
406
  // analyse, stopping at a command that merely displays its operands.
388
407
  for (let index = 0; index < args.length; index += 1) {
389
408
  const candidate = commandName(args[index] ?? "");
390
- if (candidate === "git" ||
409
+ if (ctx.wanted(candidate) ||
391
410
  candidate === "cd" ||
392
411
  candidate === "eval" ||
393
412
  SHELLS.has(candidate) ||
413
+ isPowerShell(candidate) ||
394
414
  EXEC_WRAPPERS.has(candidate)) {
395
- // xargs appends operands read from stdin; stand in "." (whole tree)
396
- // so `… | xargs git checkout --` is checked rather than parsed as empty.
397
- const child = args.slice(index);
398
- analyzeWords(name === "xargs" ? [...child, "."] : child, deeper, out);
415
+ walkWords(args.slice(index), { ...deeper, viaXargs: ctx.viaXargs || name === "xargs" });
399
416
  return;
400
417
  }
401
418
  if (NON_EXEC_COMMANDS.has(candidate))
@@ -403,6 +420,18 @@ function analyzeWords(input, ctx, out) {
403
420
  }
404
421
  }
405
422
  }
423
+ /**
424
+ * Every simple command in `command` whose name satisfies `wanted`, in source
425
+ * order, after splitting on `&&`/`;`/`||`/pipes, entering subshells,
426
+ * substitutions and `if`/`for`/`while` bodies, unwrapping `bash -c`/`eval`/
427
+ * `pwsh -c` scripts and exec wrappers (`sudo`, `env`, `timeout`, `xargs`, …),
428
+ * and following `cd`. Pure: nothing is executed.
429
+ */
430
+ export function walkShell(command, cwd, wanted) {
431
+ const out = [];
432
+ walkScript(command, { cwd, depth: 0, viaXargs: false, wanted, out });
433
+ return out;
434
+ }
406
435
  // ── git classification ─────────────────────────────────────
407
436
  /** True for a short-option cluster (`-fdx`) containing `letter`. */
408
437
  function hasShort(tokens, letter) {
@@ -474,18 +503,18 @@ function analyzeGit(args, cwd) {
474
503
  while (i < args.length) {
475
504
  const arg = args[i] ?? "";
476
505
  if (arg === "-C") {
477
- dir = path.resolve(dir, expandHome(args[i + 1] ?? "."));
506
+ dir = path.resolve(dir, expandShellPath(args[i + 1] ?? ".", dir));
478
507
  i += 2;
479
508
  continue;
480
509
  }
481
510
  if (arg === "--git-dir" || arg === "--work-tree") {
482
- gitArgs.push(`${arg}=${path.resolve(dir, expandHome(args[i + 1] ?? "."))}`);
511
+ gitArgs.push(`${arg}=${path.resolve(dir, expandShellPath(args[i + 1] ?? ".", dir))}`);
483
512
  i += 2;
484
513
  continue;
485
514
  }
486
515
  const assigned = /^(--git-dir|--work-tree)=(.*)$/.exec(arg);
487
516
  if (assigned) {
488
- gitArgs.push(`${assigned[1]}=${path.resolve(dir, expandHome(assigned[2] ?? "."))}`);
517
+ gitArgs.push(`${assigned[1]}=${path.resolve(dir, expandShellPath(assigned[2] ?? ".", dir))}`);
489
518
  i += 1;
490
519
  continue;
491
520
  }
@@ -615,39 +644,52 @@ function truncate(text, max = 120) {
615
644
  */
616
645
  export function findDestructiveGitCommands(command, cwd) {
617
646
  const out = [];
618
- analyzeScript(command, { cwd, depth: 0 }, out);
647
+ for (const call of walkShell(command, cwd, (name) => name === "git")) {
648
+ // xargs appends operands read from stdin; stand in "." (whole tree)
649
+ // so `… | xargs git checkout --` is checked rather than parsed as empty.
650
+ const match = analyzeGit(call.viaXargs ? [...call.args, "."] : call.args, call.cwd);
651
+ if (!match)
652
+ continue;
653
+ // A pathspec still holding a variable (`for f in …; do git checkout -- $f`)
654
+ // could name anything once expanded: check the whole tree instead of a
655
+ // literal `$f` that matches no file.
656
+ if (match.pathspecs.some((spec) => spec.includes("$")))
657
+ match.pathspecs = [];
658
+ out.push(match);
659
+ }
619
660
  return out;
620
661
  }
621
662
  class GuardError extends Error {
622
663
  }
623
- function runGit(dir, args) {
624
- return new Promise((resolve, reject) => {
625
- execFile("git", [...args], {
664
+ /**
665
+ * Run an inspection git command against the target repo with repo-config
666
+ * execution sinks (fsmonitor, hooks, filters, …) disabled — see
667
+ * runBackgroundGit. `gitArgs` are the command's own `--git-dir`/`--work-tree`.
668
+ */
669
+ async function runGit(dir, gitArgs, args) {
670
+ try {
671
+ const { stdout } = await runBackgroundGit(args, {
626
672
  cwd: dir,
627
- timeout: GIT_TIMEOUT_MS,
673
+ globalArgs: gitArgs,
674
+ timeoutMs: GIT_TIMEOUT_MS,
628
675
  maxBuffer: 8 * 1024 * 1024,
629
- windowsHide: true,
630
- env: { ...process.env, GIT_OPTIONAL_LOCKS: "0", LC_ALL: "C", GIT_TERMINAL_PROMPT: "0" },
631
- }, (error, stdout) => {
632
- if (!error) {
633
- resolve({ code: 0, stdout });
634
- return;
635
- }
636
- // A numeric code is git's exit status; a string (ENOENT) or a kill
637
- // (timeout) means the guard itself could not inspect the repo.
638
- const code = error.code;
639
- if (typeof code === "number" && !error.killed) {
640
- resolve({ code, stdout });
641
- return;
642
- }
643
- reject(new GuardError(`git ${args.join(" ")} failed: ${error.message}`));
676
+ env: { LC_ALL: "C" },
644
677
  });
645
- });
678
+ return { code: 0, stdout };
679
+ }
680
+ catch (error) {
681
+ // A numeric code is git's exit status; a string (ENOENT) or a kill
682
+ // (timeout) means the guard itself could not inspect the repo.
683
+ if (error instanceof BackgroundGitError && typeof error.code === "number" && !error.killed) {
684
+ return { code: error.code, stdout: error.stdout };
685
+ }
686
+ const message = error instanceof Error ? error.message : String(error);
687
+ throw new GuardError(`git ${args.join(" ")} failed: ${message}`);
688
+ }
646
689
  }
647
690
  /** `git status --porcelain=v1 -z`, parsed. Null when `dir` is not a repo. */
648
691
  async function gitStatus(match, extra) {
649
- const result = await runGit(match.dir, [
650
- ...match.gitArgs,
692
+ const result = await runGit(match.dir, match.gitArgs, [
651
693
  "status",
652
694
  "--porcelain=v1",
653
695
  "-z",
@@ -711,8 +753,7 @@ async function evaluate(match) {
711
753
  case "checkout-paths":
712
754
  case "restore-worktree": {
713
755
  if (match.maybeRef !== undefined) {
714
- const ref = await runGit(match.dir, [
715
- ...match.gitArgs,
756
+ const ref = await runGit(match.dir, match.gitArgs, [
716
757
  "rev-parse",
717
758
  "--verify",
718
759
  "--quiet",
@@ -756,7 +797,7 @@ async function evaluate(match) {
756
797
  }
757
798
  case "stash-drop":
758
799
  case "stash-clear": {
759
- const list = await runGit(match.dir, [...match.gitArgs, "stash", "list"]);
800
+ const list = await runGit(match.dir, match.gitArgs, ["stash", "list"]);
760
801
  const entries = list.code === 0 ? list.stdout.split("\n").filter((l) => l.length > 0) : [];
761
802
  if (entries.length === 0)
762
803
  return null;
@@ -772,8 +813,7 @@ async function evaluate(match) {
772
813
  const losses = [];
773
814
  for (const branch of match.refs) {
774
815
  const ref = `refs/heads/${branch}`;
775
- const result = await runGit(match.dir, [
776
- ...match.gitArgs,
816
+ const result = await runGit(match.dir, match.gitArgs, [
777
817
  "rev-list",
778
818
  "--count",
779
819
  ref,