@travisennis/acai 0.0.13 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -111,6 +111,8 @@ Reference files directly with `@filename`, directories with `@dirname`, or run s
111
111
  - [Dynamic Tools](docs/dynamic-tools.md) - Creating custom tools to extend acai (supports bash, python, and other languages; Amp-compatible text schema format; `/tools make` scaffolding command)
112
112
  - [Architecture](ARCHITECTURE.md) - Internal architecture and flow diagrams
113
113
  - [Contributing](CONTRIBUTING.md) - Development setup, scripts, and code style
114
+ - [Agent Instructions](AGENTS.md) - Routing guide for AI agents working in this repo
115
+ - [Agent Guardrails](docs/guardrails/) - Focused compatibility, security, testing, and documentation rules
114
116
 
115
117
  ## Acknowledgments
116
118
 
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../source/agent/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../source/agent/index.ts"],"names":[],"mappings":"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"}
@@ -2,6 +2,42 @@ import { generateText, InvalidToolInputError, NoOutputGeneratedError, NoSuchTool
2
2
  import { AiConfig } from "../models/ai-config.js";
3
3
  import { toAiSdkTools } from "../tools/utils.js";
4
4
  import { logger } from "../utils/logger.js";
5
+ /**
6
+ * Whether a stream chunk carries model output (used to mark time-to-first-token).
7
+ */
8
+ function isContentChunk(chunk) {
9
+ return (chunk.type === "text-delta" ||
10
+ chunk.type === "reasoning-delta" ||
11
+ chunk.type === "tool-call");
12
+ }
13
+ /**
14
+ * Rough input-token estimate for the request.start event. Real input token
15
+ * counts are reported on request.end from provider usage.
16
+ */
17
+ function estimateInputTokens(messages, systemPrompt) {
18
+ const chars = systemPrompt.length + JSON.stringify(messages).length;
19
+ return Math.ceil(chars / 4);
20
+ }
21
+ /**
22
+ * Selects provider response headers worth keeping for telemetry: request id,
23
+ * rate-limit headers, and retry hints. Header names vary per provider.
24
+ */
25
+ function extractProviderHeaders(headers) {
26
+ if (!headers) {
27
+ return {};
28
+ }
29
+ const out = {};
30
+ for (const [key, value] of Object.entries(headers)) {
31
+ const lower = key.toLowerCase();
32
+ if (lower === "x-request-id" ||
33
+ lower === "retry-after" ||
34
+ lower.includes("ratelimit") ||
35
+ lower.includes("rate-limit")) {
36
+ out[lower] = value;
37
+ }
38
+ }
39
+ return out;
40
+ }
5
41
  export class Agent {
6
42
  opts;
7
43
  config;
@@ -55,6 +91,9 @@ export class Agent {
55
91
  type: "step-start",
56
92
  };
57
93
  const toolsCalled = new Map();
94
+ const requestId = `${sessionManager.getSessionId()}:${iter}`;
95
+ const requestStart = performance.now();
96
+ let firstTokenAt = null;
58
97
  try {
59
98
  // Check abort signal again before starting streamText
60
99
  if (abortSignal?.aborted) {
@@ -62,6 +101,15 @@ export class Agent {
62
101
  }
63
102
  const messages = sessionManager.get();
64
103
  const aiSdkTools = toAiSdkTools(tools, false);
104
+ logger.info({
105
+ event: "model.request.start",
106
+ requestId,
107
+ sessionId: sessionManager.getSessionId(),
108
+ model: modelConfig.id,
109
+ provider: modelConfig.provider,
110
+ iteration: iter,
111
+ inputTokenEstimate: estimateInputTokens(messages, systemPrompt),
112
+ }, "model.request.start");
65
113
  const result = streamText({
66
114
  model: langModel,
67
115
  maxOutputTokens: aiConfig.maxOutputTokens(),
@@ -103,6 +151,14 @@ export class Agent {
103
151
  });
104
152
  const pendingToolCalls = [];
105
153
  for await (const chunk of result.fullStream) {
154
+ if (firstTokenAt === null && isContentChunk(chunk)) {
155
+ firstTokenAt = performance.now();
156
+ logger.info({
157
+ event: "model.first_token",
158
+ requestId,
159
+ ttftMs: Math.round(firstTokenAt - requestStart),
160
+ }, "model.first_token");
161
+ }
106
162
  const event = this.handleStreamChunk(chunk, {
107
163
  accumulatedText,
108
164
  accumulatedReasoning,
@@ -116,9 +172,11 @@ export class Agent {
116
172
  yield event.agentEvent;
117
173
  }
118
174
  }
175
+ const modelStreamEnd = performance.now();
119
176
  // ============================================================
120
177
  // PARALLEL TOOL EXECUTION
121
178
  // ============================================================
179
+ const toolStart = performance.now();
122
180
  const { collectedEvents, parallelToolMessages } = await this.executeToolsInParallel({
123
181
  pendingToolCalls,
124
182
  abortSignal,
@@ -126,6 +184,7 @@ export class Agent {
126
184
  stepToolCalls: thisStepToolCalls,
127
185
  stepToolResults: thisStepToolResults,
128
186
  });
187
+ const toolEnd = performance.now();
129
188
  // Yield all collected events in order
130
189
  for (const event of collectedEvents) {
131
190
  yield event;
@@ -143,6 +202,35 @@ export class Agent {
143
202
  this.updateUsageStats(stepUsage, sessionManager);
144
203
  // If finishReason is not tool-calls, break
145
204
  const finishReason = await result.finishReason;
205
+ const modelResponseMs = modelStreamEnd - requestStart;
206
+ const toolMs = toolEnd - toolStart;
207
+ const wallClockMs = performance.now() - requestStart;
208
+ const outputTokens = stepUsage.outputTokens ?? 0;
209
+ logger.info({
210
+ event: "model.request.end",
211
+ requestId,
212
+ model: modelConfig.id,
213
+ provider: modelConfig.provider,
214
+ modelResponseMs: Math.round(modelResponseMs),
215
+ ttftMs: firstTokenAt === null
216
+ ? null
217
+ : Math.round(firstTokenAt - requestStart),
218
+ inputTokens: stepUsage.inputTokens ?? 0,
219
+ outputTokens,
220
+ outputTokensPerSecond: modelResponseMs > 0
221
+ ? Number((outputTokens / (modelResponseMs / 1000)).toFixed(2))
222
+ : 0,
223
+ reasoningTokens: stepUsage.outputTokenDetails.reasoningTokens ?? 0,
224
+ finishReason,
225
+ retryCount: consecutiveErrors,
226
+ providerRequestId: response.id ?? null,
227
+ providerHeaders: extractProviderHeaders(response.headers),
228
+ }, "model.request.end");
229
+ sessionManager.recordTurnTiming({
230
+ wallClockMs,
231
+ modelMs: modelResponseMs,
232
+ toolMs,
233
+ });
146
234
  if (finishReason !== "tool-calls") {
147
235
  yield {
148
236
  type: "agent-stop",
@@ -157,6 +245,7 @@ export class Agent {
157
245
  type: "step-stop",
158
246
  };
159
247
  // continue iterations
248
+ consecutiveErrors = 0;
160
249
  iter += 1;
161
250
  }
162
251
  catch (error) {
@@ -151,7 +151,7 @@ export class ConfigManager {
151
151
  async _readConfig(configPath) {
152
152
  try {
153
153
  const data = await fs.readFile(configPath, "utf8");
154
- return jsonParser(ConfigSchema).parse(data);
154
+ return JSON.parse(data);
155
155
  }
156
156
  catch (error) {
157
157
  const code = error.code;
@@ -4,8 +4,10 @@ declare const opencodeGoModels: {
4
4
  readonly "deepseek-v4-pro": import("@ai-sdk/provider").LanguageModelV3;
5
5
  readonly "glm-5": import("@ai-sdk/provider").LanguageModelV3;
6
6
  readonly "glm-5-1": import("@ai-sdk/provider").LanguageModelV3;
7
+ readonly "glm-5-2": import("@ai-sdk/provider").LanguageModelV3;
7
8
  readonly "kimi-k2-5": import("@ai-sdk/provider").LanguageModelV3;
8
9
  readonly "kimi-k2-6": import("@ai-sdk/provider").LanguageModelV3;
10
+ readonly "kimi-k2-7-code": import("@ai-sdk/provider").LanguageModelV3;
9
11
  readonly "minimax-m2-5": import("@ai-sdk/provider").LanguageModelV3;
10
12
  readonly "minimax-m2-7": import("@ai-sdk/provider").LanguageModelV3;
11
13
  readonly "mimo-v2-5": import("@ai-sdk/provider").LanguageModelV3;
@@ -19,7 +21,7 @@ type ModelName = `opencode-go:${keyof typeof opencodeGoModels}`;
19
21
  export declare const opencodeGoModelNames: ModelName[];
20
22
  export declare const opencodeGoProvider: {
21
23
  "opencode-go": import("@ai-sdk/provider").ProviderV3 & {
22
- languageModel(modelId: "deepseek-v4-flash" | "deepseek-v4-pro" | "glm-5" | "minimax-m3" | "qwen3.6-plus" | "qwen3.7-plus" | "qwen3.7-max" | "glm-5-1" | "kimi-k2-5" | "kimi-k2-6" | "minimax-m2-5" | "minimax-m2-7" | "mimo-v2-5" | "mimo-v2-5-pro"): import("@ai-sdk/provider").LanguageModelV3;
24
+ languageModel(modelId: "deepseek-v4-flash" | "deepseek-v4-pro" | "glm-5" | "minimax-m3" | "qwen3.6-plus" | "qwen3.7-plus" | "qwen3.7-max" | "glm-5-1" | "glm-5-2" | "kimi-k2-5" | "kimi-k2-6" | "kimi-k2-7-code" | "minimax-m2-5" | "minimax-m2-7" | "mimo-v2-5" | "mimo-v2-5-pro"): import("@ai-sdk/provider").LanguageModelV3;
23
25
  embeddingModel(modelId: string): import("@ai-sdk/provider").EmbeddingModelV3;
24
26
  imageModel(modelId: string): import("@ai-sdk/provider").ImageModelV3;
25
27
  transcriptionModel(modelId: string): import("@ai-sdk/provider").TranscriptionModelV3;
@@ -1 +1 @@
1
- {"version":3,"file":"opencode-go-provider.d.ts","sourceRoot":"","sources":["../../source/models/opencode-go-provider.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAepD,QAAA,MAAM,gBAAgB;;;;;;;;;;;;;;;CAeZ,CAAC;AAEX,KAAK,SAAS,GAAG,eAAe,MAAM,OAAO,gBAAgB,EAAE,CAAC;AAEhE,eAAO,MAAM,oBAAoB,EAAE,SAAS,EAEC,CAAC;AAE9C,eAAO,MAAM,kBAAkB;;;;;;;;;;CAK9B,CAAC;AAEF,eAAO,MAAM,uBAAuB,EAAE;KACnC,CAAC,IAAI,SAAS,GAAG,aAAa,CAAC,SAAS,CAAC;CA0K3C,CAAC"}
1
+ {"version":3,"file":"opencode-go-provider.d.ts","sourceRoot":"","sources":["../../source/models/opencode-go-provider.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAepD,QAAA,MAAM,gBAAgB;;;;;;;;;;;;;;;;;CAiBZ,CAAC;AAEX,KAAK,SAAS,GAAG,eAAe,MAAM,OAAO,gBAAgB,EAAE,CAAC;AAEhE,eAAO,MAAM,oBAAoB,EAAE,SAAS,EAEC,CAAC;AAE9C,eAAO,MAAM,kBAAkB;;;;;;;;;;CAK9B,CAAC;AAEF,eAAO,MAAM,uBAAuB,EAAE;KACnC,CAAC,IAAI,SAAS,GAAG,aAAa,CAAC,SAAS,CAAC;CAkM3C,CAAC"}
@@ -18,8 +18,10 @@ const opencodeGoModels = {
18
18
  "deepseek-v4-pro": completionsClient("deepseek-v4-pro"),
19
19
  "glm-5": completionsClient("glm-5"),
20
20
  "glm-5-1": completionsClient("glm-5.1"),
21
+ "glm-5-2": completionsClient("glm-5.2"),
21
22
  "kimi-k2-5": completionsClient("kimi-k2.5"),
22
23
  "kimi-k2-6": completionsClient("kimi-k2.6"),
24
+ "kimi-k2-7-code": completionsClient("kimi-k2.7-code"),
23
25
  "minimax-m2-5": messagesClient("minimax-m2.5"),
24
26
  "minimax-m2-7": messagesClient("minimax-m2.7"),
25
27
  "mimo-v2-5": completionsClient("mimo-v2.5"),
@@ -85,6 +87,18 @@ export const opencodeGoModelRegistry = {
85
87
  costPerInputToken: 0.0000014,
86
88
  costPerOutputToken: 0.0000044,
87
89
  },
90
+ "opencode-go:glm-5-2": {
91
+ id: "opencode-go:glm-5-2",
92
+ provider: "opencode-go",
93
+ contextWindow: 202752,
94
+ maxOutputTokens: 202752,
95
+ defaultTemperature: 0.5,
96
+ promptFormat: "markdown",
97
+ supportsReasoning: true,
98
+ supportsToolCalling: true,
99
+ costPerInputToken: 0.0000014,
100
+ costPerOutputToken: 0.0000044,
101
+ },
88
102
  "opencode-go:kimi-k2-5": {
89
103
  id: "opencode-go:kimi-k2-5",
90
104
  provider: "opencode-go",
@@ -109,6 +123,18 @@ export const opencodeGoModelRegistry = {
109
123
  costPerInputToken: 0.00000095,
110
124
  costPerOutputToken: 0.000004,
111
125
  },
126
+ "opencode-go:kimi-k2-7-code": {
127
+ id: "opencode-go:kimi-k2-7-code",
128
+ provider: "opencode-go",
129
+ contextWindow: 256000,
130
+ maxOutputTokens: 65536,
131
+ defaultTemperature: 1.0,
132
+ promptFormat: "markdown",
133
+ supportsReasoning: true,
134
+ supportsToolCalling: true,
135
+ costPerInputToken: 0.00000095,
136
+ costPerOutputToken: 0.000004,
137
+ },
112
138
  "opencode-go:minimax-m2-5": {
113
139
  id: "opencode-go:minimax-m2-5",
114
140
  provider: "opencode-go",
@@ -166,8 +192,8 @@ export const opencodeGoModelRegistry = {
166
192
  promptFormat: "markdown",
167
193
  supportsReasoning: true,
168
194
  supportsToolCalling: true,
169
- costPerInputToken: 6e-7,
170
- costPerOutputToken: 0.0000024,
195
+ costPerInputToken: 0.0000003,
196
+ costPerOutputToken: 0.0000012,
171
197
  },
172
198
  "opencode-go:qwen3.6-plus": {
173
199
  id: "opencode-go:qwen3.6-plus",
@@ -1,6 +1,6 @@
1
1
  declare const providers: readonly ["anthropic", "openai", "google", "groq", "deepseek", "openrouter", "xai", "opencode", "opencode-go"];
2
2
  type ModelProvider = (typeof providers)[number];
3
- export declare const models: readonly ("anthropic:haiku" | "anthropic:opus" | "anthropic:sonnet" | "deepseek:deepseek-v4-flash" | "deepseek:deepseek-v4-pro" | "openai:gpt-5.1-codex-mini" | "openai:gpt-5.2" | "openai:gpt-5.2-codex" | "opencode-go:deepseek-v4-flash" | "opencode-go:deepseek-v4-pro" | "opencode-go:glm-5" | "opencode-go:minimax-m3" | "opencode-go:qwen3.6-plus" | "opencode-go:qwen3.7-plus" | "opencode-go:qwen3.7-max" | "opencode-go:glm-5-1" | "opencode-go:kimi-k2-5" | "opencode-go:kimi-k2-6" | "opencode-go:minimax-m2-5" | "opencode-go:minimax-m2-7" | "opencode-go:mimo-v2-5" | "opencode-go:mimo-v2-5-pro" | "opencode:glm-5" | "opencode:minimax-m2.5" | "opencode:minimax-m2.7" | "opencode:glm-5-1" | "opencode:kimi-k2-5" | "opencode:kimi-k2-6" | "opencode:opus-4-6" | "openrouter:glm-5" | "openrouter:glm-5-1" | "openrouter:kimi-k2-5" | "openrouter:kimi-k2-6" | "openrouter:minimax-m2-5" | "openrouter:minimax-m2-7" | "openrouter:gpt-oss-120b" | "openrouter:gpt-5-3-codex" | "openrouter:sonnet-4.5" | "openrouter:opus-4.6" | "openrouter:owl-alpha" | "openrouter:haiku-4.5" | "xai:grok-4-1-fast")[];
3
+ export declare const models: readonly ("anthropic:haiku" | "anthropic:opus" | "anthropic:sonnet" | "deepseek:deepseek-v4-flash" | "deepseek:deepseek-v4-pro" | "openai:gpt-5.1-codex-mini" | "openai:gpt-5.2" | "openai:gpt-5.2-codex" | "opencode-go:deepseek-v4-flash" | "opencode-go:deepseek-v4-pro" | "opencode-go:glm-5" | "opencode-go:minimax-m3" | "opencode-go:qwen3.6-plus" | "opencode-go:qwen3.7-plus" | "opencode-go:qwen3.7-max" | "opencode-go:glm-5-1" | "opencode-go:glm-5-2" | "opencode-go:kimi-k2-5" | "opencode-go:kimi-k2-6" | "opencode-go:kimi-k2-7-code" | "opencode-go:minimax-m2-5" | "opencode-go:minimax-m2-7" | "opencode-go:mimo-v2-5" | "opencode-go:mimo-v2-5-pro" | "opencode:glm-5" | "opencode:minimax-m2.5" | "opencode:minimax-m2.7" | "opencode:glm-5-1" | "opencode:kimi-k2-5" | "opencode:kimi-k2-6" | "opencode:opus-4-6" | "openrouter:glm-5" | "openrouter:glm-5-1" | "openrouter:kimi-k2-5" | "openrouter:kimi-k2-6" | "openrouter:minimax-m2-5" | "openrouter:minimax-m2-7" | "openrouter:gpt-oss-120b" | "openrouter:gpt-5-3-codex" | "openrouter:sonnet-4.5" | "openrouter:opus-4.6" | "openrouter:owl-alpha" | "openrouter:haiku-4.5" | "xai:grok-4-1-fast")[];
4
4
  export type ModelName = (typeof models)[number] | (`xai:${string}` & {}) | (`openai:${string}` & {}) | (`anthropic:${string}` & {}) | (`google:${string}` & {}) | (`groq:${string}` & {}) | (`deepseek:${string}` & {}) | (`openrouter:${string}` & {}) | (`opencode-go:${string}` & {}) | (`opencode:${string}` & {});
5
5
  export declare function isSupportedModel(model: unknown): model is ModelName;
6
6
  export declare function languageModel(model: ModelName): import("@ai-sdk/provider").LanguageModelV3;
@@ -1 +1 @@
1
- {"version":3,"file":"providers.d.ts","sourceRoot":"","sources":["../../source/models/providers.ts"],"names":[],"mappings":"AAgDA,QAAA,MAAM,SAAS,gHAUL,CAAC;AAEX,KAAK,aAAa,GAAG,CAAC,OAAO,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC;AAchD,eAAO,MAAM,MAAM,8jCAUT,CAAC;AAEX,MAAM,MAAM,SAAS,GACjB,CAAC,OAAO,MAAM,CAAC,CAAC,MAAM,CAAC,GACvB,CAAC,OAAO,MAAM,EAAE,GAAG,EAAE,CAAC,GACtB,CAAC,UAAU,MAAM,EAAE,GAAG,EAAE,CAAC,GACzB,CAAC,aAAa,MAAM,EAAE,GAAG,EAAE,CAAC,GAC5B,CAAC,UAAU,MAAM,EAAE,GAAG,EAAE,CAAC,GACzB,CAAC,QAAQ,MAAM,EAAE,GAAG,EAAE,CAAC,GACvB,CAAC,YAAY,MAAM,EAAE,GAAG,EAAE,CAAC,GAC3B,CAAC,cAAc,MAAM,EAAE,GAAG,EAAE,CAAC,GAC7B,CAAC,eAAe,MAAM,EAAE,GAAG,EAAE,CAAC,GAC9B,CAAC,YAAY,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;AAEhC,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,SAAS,CAcnE;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,8CAE7C;AAED,MAAM,WAAW,aAAa,CAAC,CAAC,GAAG,SAAS;IAC1C,EAAE,EAAE,CAAC,CAAC;IACN,QAAQ,EAAE,aAAa,CAAC;IACxB,aAAa,EAAE,MAAM,CAAC;IACtB,mBAAmB,EAAE,OAAO,CAAC;IAC7B,iBAAiB,EAAE,OAAO,CAAC;IAC3B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,eAAe,EAAE,MAAM,CAAC;IACxB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,YAAY,EAAE,KAAK,GAAG,UAAU,GAAG,SAAS,CAAC;CAC9C;AAGD,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,SAAS,EAAE,aAAa,CAU1D,CAAC;AAGF,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,IAAI,SAAS,CAEtE"}
1
+ {"version":3,"file":"providers.d.ts","sourceRoot":"","sources":["../../source/models/providers.ts"],"names":[],"mappings":"AAgDA,QAAA,MAAM,SAAS,gHAUL,CAAC;AAEX,KAAK,aAAa,GAAG,CAAC,OAAO,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC;AAchD,eAAO,MAAM,MAAM,qnCAUT,CAAC;AAEX,MAAM,MAAM,SAAS,GACjB,CAAC,OAAO,MAAM,CAAC,CAAC,MAAM,CAAC,GACvB,CAAC,OAAO,MAAM,EAAE,GAAG,EAAE,CAAC,GACtB,CAAC,UAAU,MAAM,EAAE,GAAG,EAAE,CAAC,GACzB,CAAC,aAAa,MAAM,EAAE,GAAG,EAAE,CAAC,GAC5B,CAAC,UAAU,MAAM,EAAE,GAAG,EAAE,CAAC,GACzB,CAAC,QAAQ,MAAM,EAAE,GAAG,EAAE,CAAC,GACvB,CAAC,YAAY,MAAM,EAAE,GAAG,EAAE,CAAC,GAC3B,CAAC,cAAc,MAAM,EAAE,GAAG,EAAE,CAAC,GAC7B,CAAC,eAAe,MAAM,EAAE,GAAG,EAAE,CAAC,GAC9B,CAAC,YAAY,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;AAEhC,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,SAAS,CAcnE;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,8CAE7C;AAED,MAAM,WAAW,aAAa,CAAC,CAAC,GAAG,SAAS;IAC1C,EAAE,EAAE,CAAC,CAAC;IACN,QAAQ,EAAE,aAAa,CAAC;IACxB,aAAa,EAAE,MAAM,CAAC;IACtB,mBAAmB,EAAE,OAAO,CAAC;IAC7B,iBAAiB,EAAE,OAAO,CAAC;IAC3B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,eAAe,EAAE,MAAM,CAAC;IACxB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,YAAY,EAAE,KAAK,GAAG,UAAU,GAAG,SAAS,CAAC;CAC9C;AAGD,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,SAAS,EAAE,aAAa,CAU1D,CAAC;AAGF,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,IAAI,SAAS,CAEtE"}
@@ -36,6 +36,26 @@ type SessionTokenUsage = {
36
36
  estimatedCost: number;
37
37
  };
38
38
  };
39
+ /**
40
+ * Per-turn timing accumulated across a session.
41
+ * Mirrors the SessionTokenUsage shape (total plus lastTurn) so summaries can
42
+ * separate model response time from tool execution time.
43
+ */
44
+ type SessionTiming = {
45
+ /** Aggregated totals for the entire session */
46
+ total: {
47
+ wallClockMs: number;
48
+ modelMs: number;
49
+ toolMs: number;
50
+ turns: number;
51
+ };
52
+ /** Last turn's timing */
53
+ lastTurn: {
54
+ wallClockMs: number;
55
+ modelMs: number;
56
+ toolMs: number;
57
+ };
58
+ };
39
59
  /**
40
60
  * @deprecated Use SessionTokenUsage instead. Kept for backward compatibility
41
61
  * with older session files that may have the old format.
@@ -68,6 +88,7 @@ type SavedMessageHistory = {
68
88
  updatedAt: Date;
69
89
  messages: ModelMessage[];
70
90
  tokenUsage?: SessionTokenUsage | TokenUsageTurn[];
91
+ timing?: SessionTiming;
71
92
  metadata?: Record<string, unknown>;
72
93
  };
73
94
  interface MessageHistoryEvents {
@@ -86,6 +107,7 @@ export declare class SessionManager extends EventEmitter<MessageHistoryEvents> {
86
107
  private modelManager;
87
108
  private tokenTracker;
88
109
  private tokenUsage;
110
+ private timing;
89
111
  private transientMessages;
90
112
  private metadata;
91
113
  constructor({ stateDir, modelManager, tokenTracker, }: {
@@ -94,6 +116,7 @@ export declare class SessionManager extends EventEmitter<MessageHistoryEvents> {
94
116
  tokenTracker: TokenTracker;
95
117
  });
96
118
  private createEmptyTokenUsage;
119
+ private createEmptyTiming;
97
120
  create(modelId: string): void;
98
121
  setModelId(modelId: string): void;
99
122
  private validMessage;
@@ -161,6 +184,24 @@ export declare class SessionManager extends EventEmitter<MessageHistoryEvents> {
161
184
  };
162
185
  getLastTurnContextWindow(): number;
163
186
  clearTokenUsage(): void;
187
+ recordTurnTiming(timing: {
188
+ wallClockMs: number;
189
+ modelMs: number;
190
+ toolMs: number;
191
+ }): void;
192
+ /**
193
+ * Session-level rollup of timing data. Returns null when no timing has been
194
+ * recorded for the session. `overheadMs` is non-tool, non-model wall-clock
195
+ * time, and `toolTimeRatio` is the fraction of total wall-clock spent in tools.
196
+ */
197
+ getTimingSummary(): {
198
+ wallClockMs: number;
199
+ modelMs: number;
200
+ toolMs: number;
201
+ overheadMs: number;
202
+ toolTimeRatio: number;
203
+ turns: number;
204
+ } | null;
164
205
  }
165
206
  export {};
166
207
  //# sourceMappingURL=manager.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../source/sessions/manager.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../source/sessions/manager.ts"],"names":[],"mappings":"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"}
@@ -113,6 +113,7 @@ export class SessionManager extends EventEmitter {
113
113
  modelManager;
114
114
  tokenTracker;
115
115
  tokenUsage;
116
+ timing;
116
117
  transientMessages = [];
117
118
  metadata = {};
118
119
  constructor({ stateDir, modelManager, tokenTracker, }) {
@@ -128,6 +129,7 @@ export class SessionManager extends EventEmitter {
128
129
  this.modelManager = modelManager;
129
130
  this.tokenTracker = tokenTracker;
130
131
  this.tokenUsage = null;
132
+ this.timing = null;
131
133
  }
132
134
  createEmptyTokenUsage() {
133
135
  return {
@@ -149,6 +151,21 @@ export class SessionManager extends EventEmitter {
149
151
  },
150
152
  };
151
153
  }
154
+ createEmptyTiming() {
155
+ return {
156
+ total: {
157
+ wallClockMs: 0,
158
+ modelMs: 0,
159
+ toolMs: 0,
160
+ turns: 0,
161
+ },
162
+ lastTurn: {
163
+ wallClockMs: 0,
164
+ modelMs: 0,
165
+ toolMs: 0,
166
+ },
167
+ };
168
+ }
152
169
  create(modelId) {
153
170
  this.clear();
154
171
  this.modelId = modelId;
@@ -205,6 +222,7 @@ export class SessionManager extends EventEmitter {
205
222
  this.metadata = {};
206
223
  this.contextWindow = 0;
207
224
  this.tokenUsage = null;
225
+ this.timing = null;
208
226
  this.emit("clear-history");
209
227
  }
210
228
  setContextWindow(contextWindow) {
@@ -290,6 +308,7 @@ export class SessionManager extends EventEmitter {
290
308
  updatedAt: this.updatedAt,
291
309
  messages: this.history,
292
310
  ...(this.tokenUsage !== null ? { tokenUsage: this.tokenUsage } : {}),
311
+ ...(this.timing !== null ? { timing: this.timing } : {}),
293
312
  metadata: Object.keys(this.metadata).length > 0 ? this.metadata : undefined,
294
313
  };
295
314
  try {
@@ -583,6 +602,7 @@ React Component Rendering Debug";
583
602
  else {
584
603
  this.tokenUsage = null;
585
604
  }
605
+ this.timing = savedHistory.timing ?? null;
586
606
  this.metadata = savedHistory.metadata ?? {};
587
607
  }
588
608
  // Token usage tracking methods
@@ -640,4 +660,37 @@ React Component Rendering Debug";
640
660
  clearTokenUsage() {
641
661
  this.tokenUsage = null;
642
662
  }
663
+ recordTurnTiming(timing) {
664
+ if (this.timing === null) {
665
+ this.timing = this.createEmptyTiming();
666
+ }
667
+ this.timing.total.wallClockMs += timing.wallClockMs;
668
+ this.timing.total.modelMs += timing.modelMs;
669
+ this.timing.total.toolMs += timing.toolMs;
670
+ this.timing.total.turns += 1;
671
+ this.timing.lastTurn.wallClockMs = timing.wallClockMs;
672
+ this.timing.lastTurn.modelMs = timing.modelMs;
673
+ this.timing.lastTurn.toolMs = timing.toolMs;
674
+ }
675
+ /**
676
+ * Session-level rollup of timing data. Returns null when no timing has been
677
+ * recorded for the session. `overheadMs` is non-tool, non-model wall-clock
678
+ * time, and `toolTimeRatio` is the fraction of total wall-clock spent in tools.
679
+ */
680
+ getTimingSummary() {
681
+ if (this.timing === null) {
682
+ return null;
683
+ }
684
+ const { wallClockMs, modelMs, toolMs, turns } = this.timing.total;
685
+ const overheadMs = Math.max(0, wallClockMs - modelMs - toolMs);
686
+ const toolTimeRatio = wallClockMs > 0 ? toolMs / wallClockMs : 0;
687
+ return {
688
+ wallClockMs,
689
+ modelMs,
690
+ toolMs,
691
+ overheadMs,
692
+ toolTimeRatio,
693
+ turns,
694
+ };
695
+ }
643
696
  }
@@ -1 +1 @@
1
- {"version":3,"file":"summary.d.ts","sourceRoot":"","sources":["../../source/sessions/summary.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AA4BnD,wBAAgB,gBAAgB,CAC9B,cAAc,EAAE,cAAc,EAC9B,SAAS,CAAC,EAAE,OAAO,GAClB,IAAI,CAIN;AAED,wBAAgB,gBAAgB,CAC9B,cAAc,EAAE,cAAc,EAC9B,SAAS,CAAC,EAAE,OAAO,GAClB,IAAI,CAGN"}
1
+ {"version":3,"file":"summary.d.ts","sourceRoot":"","sources":["../../source/sessions/summary.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAgDnD,wBAAgB,gBAAgB,CAC9B,cAAc,EAAE,cAAc,EAC9B,SAAS,CAAC,EAAE,OAAO,GAClB,IAAI,CAIN;AAED,wBAAgB,gBAAgB,CAC9B,cAAc,EAAE,cAAc,EAC9B,SAAS,CAAC,EAAE,OAAO,GAClB,IAAI,CAGN"}
@@ -16,9 +16,27 @@ function formatSummary(sessionManager, noSession) {
16
16
  lines.push(` File: ${filePath}`);
17
17
  lines.push(` Resume: acai --resume ${sessionId}`);
18
18
  }
19
+ const timing = sessionManager.getTimingSummary();
20
+ if (timing && timing.wallClockMs > 0) {
21
+ const toolPct = (timing.toolTimeRatio * 100).toFixed(1);
22
+ lines.push("");
23
+ lines.push(" Timing");
24
+ lines.push(` Turns: ${timing.turns}`);
25
+ lines.push(` Total: ${formatMs(timing.wallClockMs)}`);
26
+ lines.push(` Model: ${formatMs(timing.modelMs)}`);
27
+ lines.push(` Tools: ${formatMs(timing.toolMs)}`);
28
+ lines.push(` Overhead: ${formatMs(timing.overheadMs)}`);
29
+ lines.push(` Tool/Total: ${toolPct}%`);
30
+ }
19
31
  lines.push("");
20
32
  return lines.join("\n");
21
33
  }
34
+ function formatMs(ms) {
35
+ if (ms < 1000) {
36
+ return `${Math.round(ms)}ms`;
37
+ }
38
+ return `${(ms / 1000).toFixed(2)}s`;
39
+ }
22
40
  export function writeExitSummary(sessionManager, noSession) {
23
41
  const summary = formatSummary(sessionManager, noSession);
24
42
  const summaryPath = join(tmpdir(), "acai-exit-summary.txt");
@@ -1 +1 @@
1
- {"version":3,"file":"bash.d.ts","sourceRoot":"","sources":["../../source/utils/bash.ts"],"names":[],"mappings":"AAkNA,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EAAE,EACrB,GAAG,EAAE,MAAM,GACV;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAiBtC;AAiFD,eAAO,MAAM,UAAU,GACrB,UAAU,MAAM,GAAG,IAAI,GAAG,SAAS,EACnC,YAAY,MAAM,EAClB,cAAc,MAAM,EAAE,KACrB,MAaF,CAAC;AA8EF,eAAO,MAAM,iBAAiB,GAAI,YAAY,MAAM,KAAG,OAetD,CAAC"}
1
+ {"version":3,"file":"bash.d.ts","sourceRoot":"","sources":["../../source/utils/bash.ts"],"names":[],"mappings":"AAgQA,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EAAE,EACrB,GAAG,EAAE,MAAM,GACV;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAiBtC;AAiFD,eAAO,MAAM,UAAU,GACrB,UAAU,MAAM,GAAG,IAAI,GAAG,SAAS,EACnC,YAAY,MAAM,EAClB,cAAc,MAAM,EAAE,KACrB,MAaF,CAAC;AA8EF,eAAO,MAAM,iBAAiB,GAAI,YAAY,MAAM,KAAG,OAetD,CAAC"}
@@ -131,36 +131,71 @@ function computeSkipTokenMask(tokens) {
131
131
  }
132
132
  return skip;
133
133
  }
134
- function validateToken(token, allowedDirs, cwd) {
135
- // Skip fully quoted tokens - they're string arguments, not paths
136
- // (paths are typically unquoted or only quoted to handle spaces)
137
- if (isFullyQuoted(token) && token.includes("\n")) {
138
- return { isValid: true };
134
+ // Extract a path embedded in a flag-style argument (--key=value or -X/path).
135
+ // Returns null if no path-like segment is found.
136
+ function extractPathFromFlag(flag) {
137
+ // --key=value style: extract and de-quote the value side
138
+ const eqIndex = flag.indexOf("=");
139
+ if (eqIndex !== -1) {
140
+ const value = stripQuotes(flag.slice(eqIndex + 1));
141
+ // Only treat as a path if it starts with a path-identifying prefix
142
+ // to avoid validating string arguments that happen to contain "/"
143
+ if (value.startsWith("/") ||
144
+ value.startsWith("~") ||
145
+ value.startsWith("./") ||
146
+ value.startsWith("../")) {
147
+ return value;
148
+ }
149
+ return null;
139
150
  }
140
- // Remove quotes for path checking
141
- const cleanToken = stripQuotes(token);
142
- // Skip if it's clearly not a path
143
- if (cleanToken.startsWith("-") ||
144
- cleanToken.includes("://") ||
145
- (!cleanToken.includes("/") && cleanToken !== "~")) {
146
- return { isValid: true };
151
+ // -X/path style: single letter flag directly attached to a path
152
+ if (/^-[a-zA-Z]\//.test(flag)) {
153
+ return flag.slice(2);
147
154
  }
155
+ return null;
156
+ }
157
+ function validatePathValue(rawPath, allowedDirs, cwd) {
148
158
  try {
149
159
  // Expand ~ to home directory for proper validation
150
- const expandedToken = cleanToken.startsWith("~/") || cleanToken === "~"
151
- ? path.join(os.homedir(), cleanToken.slice(1))
152
- : cleanToken;
153
- const resolvedPath = path.resolve(cwd, expandedToken);
160
+ const expandedPath = rawPath.startsWith("~/") || rawPath === "~"
161
+ ? path.join(os.homedir(), rawPath.slice(1))
162
+ : rawPath;
163
+ const resolvedPath = path.resolve(cwd, expandedPath);
154
164
  if (!isPathWithinAllowedDirs(resolvedPath, allowedDirs)) {
155
165
  return {
156
166
  isValid: false,
157
- error: `Path '${cleanToken}' resolves outside the allowed directories (${resolvedPath}). All paths must be within ${allowedDirs.join(", ")}`,
167
+ error: `Path '${rawPath}' resolves outside the allowed directories (${resolvedPath}). All paths must be within ${allowedDirs.join(", ")}`,
158
168
  };
159
169
  }
160
170
  }
161
171
  catch (_e) { }
162
172
  return { isValid: true };
163
173
  }
174
+ function validateToken(token, allowedDirs, cwd) {
175
+ // Skip fully quoted tokens - they're string arguments, not paths
176
+ // (paths are typically unquoted or only quoted to handle spaces)
177
+ if (isFullyQuoted(token)) {
178
+ return { isValid: true };
179
+ }
180
+ // Remove quotes for path checking
181
+ const cleanToken = stripQuotes(token);
182
+ // Skip URLs and non-path tokens
183
+ if (cleanToken.includes("://") ||
184
+ (!cleanToken.includes("/") && cleanToken !== "~")) {
185
+ return { isValid: true };
186
+ }
187
+ // For flag-style tokens that embed paths (e.g., --file=/etc/passwd, -o/tmp/x),
188
+ // extract the embedded path for validation.
189
+ if (cleanToken.startsWith("-")) {
190
+ const embeddedPath = extractPathFromFlag(cleanToken);
191
+ if (!embeddedPath) {
192
+ // No embedded path found — just a flag
193
+ return { isValid: true };
194
+ }
195
+ return validatePathValue(embeddedPath, allowedDirs, cwd);
196
+ }
197
+ return validatePathValue(cleanToken, allowedDirs, cwd);
198
+ }
164
199
  // Validate path arguments to ensure they're within the project
165
200
  export function validatePaths(command, allowedDirs, cwd) {
166
201
  const tokens = tokenizeShellWords(command);
@@ -1,7 +1,3 @@
1
- /**
2
- * Command Protection Module
3
- * Detects and blocks destructive commands that could cause data loss
4
- */
5
1
  export interface BlockedCommandResult {
6
2
  blocked: true;
7
3
  reason: string;
@@ -1 +1 @@
1
- {"version":3,"file":"command-protection.d.ts","sourceRoot":"","sources":["../../source/utils/command-protection.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,IAAI,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,UAAU,iBAAiB;IACzB,OAAO,EAAE,KAAK,CAAC;CAChB;AAED;;GAEG;AACH,KAAK,mBAAmB,GAAG,oBAAoB,GAAG,iBAAiB,CAAC;AA0EpE;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,mBAAmB,CA4B7E;AAqXD;;GAEG;AACH,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,oBAAoB,GAC3B,MAAM,CAQR"}
1
+ {"version":3,"file":"command-protection.d.ts","sourceRoot":"","sources":["../../source/utils/command-protection.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,IAAI,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,UAAU,iBAAiB;IACzB,OAAO,EAAE,KAAK,CAAC;CAChB;AAoBD;;GAEG;AACH,KAAK,mBAAmB,GAAG,oBAAoB,GAAG,iBAAiB,CAAC;AA0EpE;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,mBAAmB,CA4B7E;AA0fD;;GAEG;AACH,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,oBAAoB,GAC3B,MAAM,CAQR"}
@@ -2,6 +2,7 @@
2
2
  * Command Protection Module
3
3
  * Detects and blocks destructive commands that could cause data loss
4
4
  */
5
+ import path from "node:path";
5
6
  /**
6
7
  * Check if a git subcommand appears as an actual command (not inside quoted strings).
7
8
  * This helps avoid false positives when destructive command text appears in
@@ -198,49 +199,168 @@ function detectDestructiveGitCommands(command) {
198
199
  * Detect dangerous rm -rf commands outside of temporary directories
199
200
  */
200
201
  function detectDangerousRmRf(command) {
201
- // Check for rm -rf pattern (case insensitive)
202
- const rmMatch = command.match(/rm\s+-rf\s+/i);
203
- if (!rmMatch || rmMatch.index === undefined) {
204
- return { blocked: false };
202
+ const tokens = tokenizeShellCommand(command);
203
+ for (let i = 0; i < tokens.length; i++) {
204
+ const token = tokens[i];
205
+ if (!token || token.separator || token.value !== "rm")
206
+ continue;
207
+ const rmArgs = collectCommandArguments(tokens, i + 1);
208
+ if (!isRecursiveForceRm(rmArgs))
209
+ continue;
210
+ const paths = getRmPathArguments(rmArgs);
211
+ if (paths.length === 0)
212
+ continue;
213
+ if (paths.every(isTempDirectoryOnly))
214
+ continue;
215
+ return {
216
+ blocked: true,
217
+ reason: "rm -rf outside of temporary directories can cause permanent data loss",
218
+ command,
219
+ tip: "Only rm -rf is allowed for /tmp/*, /var/tmp/*, or $TMPDIR/* to clean temporary files.",
220
+ };
205
221
  }
206
- // Extract the path argument
207
- const matchIndex = rmMatch.index + rmMatch[0].length;
208
- const afterRmRf = command.substring(matchIndex).trim();
209
- // If no path specified, don't block (will fail naturally)
210
- if (!afterRmRf) {
211
- return { blocked: false };
222
+ return { blocked: false };
223
+ }
224
+ function tokenizeShellCommand(command) {
225
+ const state = {
226
+ tokens: [],
227
+ current: "",
228
+ inSingleQuote: false,
229
+ inDoubleQuote: false,
230
+ escaped: false,
231
+ };
232
+ for (let i = 0; i < command.length; i++) {
233
+ consumeShellChar(state, command[i] ?? "");
234
+ }
235
+ pushShellToken(state);
236
+ return state.tokens;
237
+ }
238
+ function consumeShellChar(state, char) {
239
+ if (state.escaped) {
240
+ state.current += char;
241
+ state.escaped = false;
242
+ return;
243
+ }
244
+ if (char === "\\" && !state.inSingleQuote) {
245
+ state.escaped = true;
246
+ return;
212
247
  }
213
- // Get temporary directory paths
214
- const tempDirs = ["/tmp", "/var/tmp", process.env["TMPDIR"] || "/tmp"];
215
- // Check if the path is explicitly targeting only temp directories
216
- // This handles: rm -rf /tmp/*, rm -rf /var/tmp/*, rm -rf $TMPDIR/*, and any subpaths
217
- const isTempDirectoryOnly = tempDirs.some((tempDir) => {
218
- // Check if path starts with temp directory (e.g., /tmp/foo, /tmp/*)
219
- if (afterRmRf === tempDir || afterRmRf.startsWith(`${tempDir}/`)) {
220
- return true;
248
+ if (char === "'" && !state.inDoubleQuote) {
249
+ state.inSingleQuote = !state.inSingleQuote;
250
+ return;
251
+ }
252
+ if (char === '"' && !state.inSingleQuote) {
253
+ state.inDoubleQuote = !state.inDoubleQuote;
254
+ return;
255
+ }
256
+ if (isTokenBoundary(state, char)) {
257
+ pushShellToken(state);
258
+ if (char === "\n" || isShellSeparator(char)) {
259
+ state.tokens.push({ value: char, separator: true });
221
260
  }
222
- return false;
223
- });
224
- // Also check for $TMPDIR/* pattern (literal strings)
225
- // Using string concatenation to avoid lint warnings about template-like strings
261
+ return;
262
+ }
263
+ state.current += char;
264
+ }
265
+ function isTokenBoundary(state, char) {
266
+ return (!state.inSingleQuote &&
267
+ !state.inDoubleQuote &&
268
+ (/\s/.test(char) || isShellSeparator(char)));
269
+ }
270
+ function pushShellToken(state) {
271
+ if (!state.current)
272
+ return;
273
+ state.tokens.push({ value: state.current, separator: false });
274
+ state.current = "";
275
+ }
276
+ function isShellSeparator(char) {
277
+ return char === ";" || char === "|" || char === "&";
278
+ }
279
+ function collectCommandArguments(tokens, startIndex) {
280
+ const args = [];
281
+ for (let i = startIndex; i < tokens.length; i++) {
282
+ const token = tokens[i];
283
+ if (!token || token.separator)
284
+ break;
285
+ args.push(token.value);
286
+ }
287
+ return args;
288
+ }
289
+ function isRecursiveForceRm(args) {
290
+ const flags = { recursive: false, force: false };
291
+ for (const arg of args) {
292
+ if (arg === "--")
293
+ break;
294
+ updateRmFlagState(flags, arg);
295
+ }
296
+ return flags.recursive && flags.force;
297
+ }
298
+ function updateRmFlagState(flags, arg) {
299
+ if (arg === "--recursive") {
300
+ flags.recursive = true;
301
+ return;
302
+ }
303
+ if (arg === "--force") {
304
+ flags.force = true;
305
+ return;
306
+ }
307
+ if (!/^-[^-]+$/.test(arg))
308
+ return;
309
+ if (arg.includes("r") || arg.includes("R"))
310
+ flags.recursive = true;
311
+ if (arg.includes("f"))
312
+ flags.force = true;
313
+ }
314
+ function getRmPathArguments(args) {
315
+ const paths = [];
316
+ let parsingOptions = true;
317
+ for (const arg of args) {
318
+ if (parsingOptions && arg === "--") {
319
+ parsingOptions = false;
320
+ continue;
321
+ }
322
+ if (parsingOptions && isRmOption(arg))
323
+ continue;
324
+ paths.push(arg);
325
+ }
326
+ return paths;
327
+ }
328
+ function isRmOption(arg) {
329
+ return arg.startsWith("-") && arg !== "-";
330
+ }
331
+ function isTempDirectoryOnly(inputPath) {
226
332
  const tmpDirVar = "$" + "TMPDIR";
227
333
  const tmpDirVarBraces = "$" + "{TMPDIR}";
228
- if (afterRmRf === tmpDirVar ||
229
- afterRmRf.startsWith(`${tmpDirVar}/`) ||
230
- afterRmRf === tmpDirVarBraces ||
231
- afterRmRf.startsWith(`${tmpDirVarBraces}/`)) {
232
- return { blocked: false };
334
+ if (isTempVariablePath(inputPath, tmpDirVar) ||
335
+ isTempVariablePath(inputPath, tmpDirVarBraces)) {
336
+ return true;
233
337
  }
234
- if (isTempDirectoryOnly) {
235
- return { blocked: false };
338
+ if (!inputPath.startsWith("/"))
339
+ return false;
340
+ const normalizedPath = normalizeAbsolutePath(inputPath);
341
+ const tempDirs = ["/tmp", "/var/tmp", process.env["TMPDIR"] || "/tmp"].map(normalizeAbsolutePath);
342
+ return tempDirs.some((tempDir) => normalizedPath === tempDir || normalizedPath.startsWith(`${tempDir}/`));
343
+ }
344
+ function isTempVariablePath(inputPath, variable) {
345
+ if (inputPath !== variable && !inputPath.startsWith(`${variable}/`)) {
346
+ return false;
236
347
  }
237
- // Block any other rm -rf
238
- return {
239
- blocked: true,
240
- reason: "rm -rf outside of temporary directories can cause permanent data loss",
241
- command,
242
- tip: "Only rm -rf is allowed for /tmp/*, /var/tmp/*, or $TMPDIR/* to clean temporary files.",
243
- };
348
+ const suffix = inputPath.slice(variable.length);
349
+ return !suffix.split("/").includes("..");
350
+ }
351
+ function normalizeAbsolutePath(inputPath) {
352
+ return path.posix.normalize(inputPath.replace(/\/+$/, "") || "/");
353
+ }
354
+ function detectDangerousRmRfInText(content) {
355
+ const rmCommandPattern = /\brm\b[^\n;&|)]*/gi;
356
+ let match = rmCommandPattern.exec(content);
357
+ while (match !== null) {
358
+ const rmResult = detectDangerousRmRf(match[0]);
359
+ if (rmResult.blocked)
360
+ return rmResult;
361
+ match = rmCommandPattern.exec(content);
362
+ }
363
+ return { blocked: false };
244
364
  }
245
365
  /**
246
366
  * Detect dangerous patterns in inline scripts (-c flags)
@@ -268,9 +388,6 @@ function detectDangerousInlineScripts(command) {
268
388
  /git\s+push\s+(-f|--force)/i,
269
389
  /git\s+branch\s+-[A-Z]/i,
270
390
  /git\s+stash\s+(drop|clear)/i,
271
- /rm\s+-rf\s+\/home/i,
272
- /rm\s+-rf\s+\/usr/i,
273
- /rm\s+-rf\s+~/i,
274
391
  ];
275
392
  // Check if any destructive pattern is present in the command
276
393
  for (const destructivePattern of destructivePatterns) {
@@ -283,6 +400,15 @@ function detectDangerousInlineScripts(command) {
283
400
  };
284
401
  }
285
402
  }
403
+ const rmResult = detectDangerousRmRfInText(command);
404
+ if (rmResult.blocked) {
405
+ return {
406
+ blocked: true,
407
+ reason: `Inline ${language} script contains destructive operation`,
408
+ command,
409
+ tip: "Review the script content for destructive commands.",
410
+ };
411
+ }
286
412
  }
287
413
  }
288
414
  return { blocked: false };
@@ -371,34 +497,12 @@ function detectDangerousScriptContent(content) {
371
497
  };
372
498
  }
373
499
  }
374
- // Check for dangerous rm -rf patterns (more permissive for scripts, block obvious dangers)
375
- const dangerousRmPatterns = [
376
- /\brm\s+-rf\s+\/[^\s*]*[a-z]/i, // rm -rf /something (but allow /tmp/* patterns)
377
- /\brm\s+-rf\s+\/home/i,
378
- /\brm\s+-rf\s+\/usr/i,
379
- /\brm\s+-rf\s+\/etc/i,
380
- /\brm\s+-rf\s+\/var\s*$/i, // Block /var alone but allow /var/tmp
381
- /\brm\s+-rf\s+~(?!\/)/i, // rm -rf ~ but allow ~/tmp
382
- ];
383
- for (const pattern of dangerousRmPatterns) {
384
- if (pattern.test(content)) {
385
- return {
386
- blocked: true,
387
- tip: "The script contains a dangerous rm -rf command.",
388
- };
389
- }
390
- }
391
- // Check for format string attacks and other dangerous patterns
392
- const dangerousPatterns = [
393
- /\brm\s+-rf\s+\$\w+/i, // rm -rf $VAR (variable expansion could be dangerous)
394
- ];
395
- for (const pattern of dangerousPatterns) {
396
- if (pattern.test(content)) {
397
- return {
398
- blocked: true,
399
- tip: "The script contains a potentially dangerous rm command with variable expansion.",
400
- };
401
- }
500
+ const rmResult = detectDangerousRmRfInText(content);
501
+ if (rmResult.blocked) {
502
+ return {
503
+ blocked: true,
504
+ tip: "The script contains a dangerous rm -rf command.",
505
+ };
402
506
  }
403
507
  return { blocked: false };
404
508
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travisennis/acai",
3
- "version": "0.0.13",
3
+ "version": "0.0.14",
4
4
  "description": "An AI assistant for developing software.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -60,22 +60,22 @@
60
60
  "lint:length": "find source -name '*.ts' | xargs -I{} awk 'END{if(NR>500)print FILENAME\": \"NR\" lines\"}' {}"
61
61
  },
62
62
  "dependencies": {
63
- "@ai-sdk/anthropic": "^3.0.81",
64
- "@ai-sdk/deepseek": "^2.0.35",
65
- "@ai-sdk/devtools": "^0.0.18",
66
- "@ai-sdk/google": "^3.0.80",
67
- "@ai-sdk/groq": "^3.0.39",
68
- "@ai-sdk/open-responses": "^1.0.16",
69
- "@ai-sdk/openai": "^3.0.67",
70
- "@ai-sdk/openai-compatible": "^2.0.48",
63
+ "@ai-sdk/anthropic": "^3.0.84",
64
+ "@ai-sdk/deepseek": "^2.0.38",
65
+ "@ai-sdk/devtools": "^0.0.19",
66
+ "@ai-sdk/google": "^3.0.82",
67
+ "@ai-sdk/groq": "^3.0.41",
68
+ "@ai-sdk/open-responses": "^1.0.18",
69
+ "@ai-sdk/openai": "^3.0.71",
70
+ "@ai-sdk/openai-compatible": "^2.0.50",
71
71
  "@crosscopy/clipboard": "^0.3.6",
72
72
  "@travisennis/stdlib": "^0.0.14",
73
- "ai": "^6.0.195",
73
+ "ai": "^6.0.204",
74
74
  "cheerio": "^1.2.0",
75
75
  "diff": "^9.0.0",
76
76
  "fdir": "^6.5.0",
77
77
  "highlight.js": "^11.11.1",
78
- "marked": "18.0.4",
78
+ "marked": "18.0.5",
79
79
  "p-throttle": "^8.1.0",
80
80
  "parse5": "^8.0.1",
81
81
  "parse5-htmlparser2-tree-adapter": "^8.0.1",
@@ -87,16 +87,16 @@
87
87
  },
88
88
  "devDependencies": {
89
89
  "@ai-sdk/provider": "^3.0.10",
90
- "@biomejs/biome": "2.4.16",
90
+ "@biomejs/biome": "2.5.0",
91
91
  "@commitlint/config-conventional": "^21.0.2",
92
- "@types/node": "^25.9.1",
92
+ "@types/node": "^25.9.3",
93
93
  "c8": "^11.0.0",
94
94
  "commitlint": "^21.0.2",
95
95
  "domhandler": "^6.0.1",
96
- "fallow": "^2.88.0",
96
+ "fallow": "^2.96.0",
97
97
  "husky": "^9.1.7",
98
- "knip": "^6.15.0",
99
- "oxlint": "^1.68.0",
98
+ "knip": "^6.16.1",
99
+ "oxlint": "^1.69.0",
100
100
  "oxlint-tsgolint": "^0.23.0",
101
101
  "typescript": "^6.0.3"
102
102
  },