@travisennis/acai 0.0.11 → 0.0.12
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 +2 -3
- package/dist/commands/init-project/utils.d.ts.map +1 -1
- package/dist/commands/init-project/utils.js +0 -11
- package/dist/commands/manager.d.ts.map +1 -1
- package/dist/commands/manager.js +6 -1
- package/dist/commands/resources/index.d.ts.map +1 -1
- package/dist/commands/resources/index.js +4 -1
- package/dist/commands/session/index.d.ts.map +1 -1
- package/dist/commands/session/index.js +6 -0
- package/dist/commands/session/types.d.ts +1 -0
- package/dist/commands/session/types.d.ts.map +1 -1
- package/dist/commands/tools/index.d.ts +3 -0
- package/dist/commands/tools/index.d.ts.map +1 -0
- package/dist/commands/tools/index.js +190 -0
- package/dist/commands/tools/templates.d.ts +6 -0
- package/dist/commands/tools/templates.d.ts.map +1 -0
- package/dist/commands/tools/templates.js +97 -0
- package/dist/config/index.d.ts +5 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +41 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -3
- package/dist/models/anthropic-provider.d.ts +1 -1
- package/dist/models/deepseek-provider.d.ts +3 -3
- package/dist/models/deepseek-provider.js +17 -17
- package/dist/models/google-provider.d.ts +2 -4
- package/dist/models/google-provider.d.ts.map +1 -1
- package/dist/models/google-provider.js +2 -17
- package/dist/models/groq-provider.d.ts +2 -4
- package/dist/models/groq-provider.d.ts.map +1 -1
- package/dist/models/groq-provider.js +3 -21
- package/dist/models/opencode-go-provider.d.ts +11 -1
- package/dist/models/opencode-go-provider.d.ts.map +1 -1
- package/dist/models/opencode-go-provider.js +136 -0
- package/dist/models/opencode-zen-provider.d.ts +3 -3
- package/dist/models/opencode-zen-provider.d.ts.map +1 -1
- package/dist/models/opencode-zen-provider.js +26 -32
- package/dist/models/openrouter-provider.d.ts +4 -15
- package/dist/models/openrouter-provider.d.ts.map +1 -1
- package/dist/models/openrouter-provider.js +26 -169
- package/dist/models/providers.d.ts +1 -1
- package/dist/models/providers.d.ts.map +1 -1
- package/dist/models/xai-provider.d.ts +1 -2
- package/dist/models/xai-provider.d.ts.map +1 -1
- package/dist/models/xai-provider.js +0 -13
- package/dist/prompts/manager.d.ts.map +1 -1
- package/dist/prompts/manager.js +5 -1
- package/dist/prompts/system-prompt.d.ts +1 -0
- package/dist/prompts/system-prompt.d.ts.map +1 -1
- package/dist/prompts/system-prompt.js +20 -5
- package/dist/repl/index.d.ts +1 -2
- package/dist/repl/index.d.ts.map +1 -1
- package/dist/repl/index.js +5 -52
- package/dist/skills/activated-tracker.d.ts +11 -0
- package/dist/skills/activated-tracker.d.ts.map +1 -0
- package/dist/skills/activated-tracker.js +16 -0
- package/dist/skills/index.d.ts +1 -1
- package/dist/skills/index.d.ts.map +1 -1
- package/dist/skills/index.js +7 -1
- package/dist/tools/bash.d.ts +4 -4
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +17 -6
- package/dist/tools/directory-tree.d.ts +4 -4
- package/dist/tools/directory-tree.d.ts.map +1 -1
- package/dist/tools/directory-tree.js +2 -0
- package/dist/tools/dynamic-tool-loader.d.ts +11 -2
- package/dist/tools/dynamic-tool-loader.d.ts.map +1 -1
- package/dist/tools/dynamic-tool-loader.js +299 -39
- package/dist/tools/edit-file.d.ts +2 -2
- package/dist/tools/glob.d.ts +16 -16
- package/dist/tools/glob.d.ts.map +1 -1
- package/dist/tools/glob.js +9 -1
- package/dist/tools/grep.d.ts +14 -14
- package/dist/tools/grep.d.ts.map +1 -1
- package/dist/tools/grep.js +7 -0
- package/dist/tools/index.d.ts +42 -36
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +16 -1
- package/dist/tools/ls.d.ts +2 -2
- package/dist/tools/ls.d.ts.map +1 -1
- package/dist/tools/ls.js +1 -0
- package/dist/tools/read-file.d.ts +8 -8
- package/dist/tools/save-file.d.ts +4 -4
- package/dist/tools/skill.d.ts +2 -1
- package/dist/tools/skill.d.ts.map +1 -1
- package/dist/tools/skill.js +55 -12
- package/dist/tools/types.d.ts +8 -2
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/web-fetch.d.ts +6 -6
- package/dist/tools/web-fetch.d.ts.map +1 -1
- package/dist/tools/web-fetch.js +27 -8
- package/dist/tools/web-search.d.ts +4 -4
- package/dist/tools/web-search.js +1 -1
- package/dist/tui/components/footer.d.ts +0 -2
- package/dist/tui/components/footer.d.ts.map +1 -1
- package/dist/tui/components/footer.js +1 -17
- package/dist/utils/binary-output.d.ts +32 -0
- package/dist/utils/binary-output.d.ts.map +1 -0
- package/dist/utils/binary-output.js +127 -0
- package/dist/utils/command-protection.d.ts.map +1 -1
- package/dist/utils/command-protection.js +92 -9
- package/dist/utils/parsing.d.ts +1 -1
- package/dist/utils/parsing.d.ts.map +1 -1
- package/package.json +27 -25
- package/dist/modes/manager.d.ts +0 -24
- package/dist/modes/manager.d.ts.map +0 -1
- package/dist/modes/manager.js +0 -77
- package/dist/modes/prompts.d.ts +0 -2
- package/dist/modes/prompts.d.ts.map +0 -1
- package/dist/modes/prompts.js +0 -142
package/dist/tools/web-fetch.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
1
4
|
import { load } from "cheerio";
|
|
2
5
|
import { z } from "zod";
|
|
3
6
|
import style from "../terminal/style.js";
|
|
@@ -9,6 +12,7 @@ const DEFAULT_TIMEOUT = 30000; // 30 seconds
|
|
|
9
12
|
const MAX_REDIRECTS = 5;
|
|
10
13
|
const MAX_URL_LENGTH = 2048;
|
|
11
14
|
const JINA_API_BASE = "https://r.jina.ai";
|
|
15
|
+
const MAX_INLINE_SIZE = 50_000; // bytes; content exceeding this is written to a temp file
|
|
12
16
|
/**
|
|
13
17
|
* Input schema for the web fetch tool
|
|
14
18
|
*/
|
|
@@ -400,6 +404,7 @@ export async function executeWebFetch(options, executionOptions) {
|
|
|
400
404
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
401
405
|
throw new Error(`Web fetch failed: ${errorMessage}`);
|
|
402
406
|
}
|
|
407
|
+
let content;
|
|
403
408
|
switch (output) {
|
|
404
409
|
case "json": {
|
|
405
410
|
const jsonOutput = {
|
|
@@ -413,18 +418,32 @@ export async function executeWebFetch(options, executionOptions) {
|
|
|
413
418
|
if (headers && result.headers) {
|
|
414
419
|
jsonOutput["headers"] = result["headers"];
|
|
415
420
|
}
|
|
416
|
-
|
|
421
|
+
content = JSON.stringify(jsonOutput, null, 2);
|
|
422
|
+
break;
|
|
417
423
|
}
|
|
418
424
|
case "html":
|
|
419
|
-
|
|
425
|
+
content = result.content;
|
|
426
|
+
break;
|
|
420
427
|
case "markdown":
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
428
|
+
content = result.contentType.includes("text/html")
|
|
429
|
+
? htmlToMarkdown(result.content)
|
|
430
|
+
: result.content;
|
|
431
|
+
break;
|
|
425
432
|
default:
|
|
426
|
-
|
|
433
|
+
content = result.content;
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
const contentSize = Buffer.byteLength(content, "utf-8");
|
|
437
|
+
if (contentSize > MAX_INLINE_SIZE) {
|
|
438
|
+
const tmpFile = join(tmpdir(), `acai-webfetch-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.txt`);
|
|
439
|
+
writeFileSync(tmpFile, content, "utf-8");
|
|
440
|
+
return [
|
|
441
|
+
`The fetched content from ${url} is ${contentSize} bytes which exceeds the ${MAX_INLINE_SIZE} byte inline limit.`,
|
|
442
|
+
`The full content has been saved to: ${tmpFile}`,
|
|
443
|
+
"To use this content, read the file in parts using the Read tool or search it with the Grep tool.",
|
|
444
|
+
].join("\n");
|
|
427
445
|
}
|
|
446
|
+
return content;
|
|
428
447
|
}
|
|
429
448
|
/**
|
|
430
449
|
* Create the web fetch tool
|
|
@@ -437,7 +456,7 @@ export const createWebFetchTool = async () => {
|
|
|
437
456
|
inputSchema,
|
|
438
457
|
},
|
|
439
458
|
display({ url }) {
|
|
440
|
-
return
|
|
459
|
+
return `${style.cyan(url)}`;
|
|
441
460
|
},
|
|
442
461
|
async execute(options, executionOptions) {
|
|
443
462
|
return executeWebFetch(options, executionOptions);
|
|
@@ -8,8 +8,8 @@ export declare const WebSearchTool: {
|
|
|
8
8
|
*/
|
|
9
9
|
declare const inputSchema: z.ZodObject<{
|
|
10
10
|
query: z.ZodString;
|
|
11
|
-
numResults: z.ZodOptional<z.
|
|
12
|
-
timeout: z.ZodOptional<z.
|
|
11
|
+
numResults: z.ZodOptional<z.ZodPreprocess<z.ZodNullable<z.ZodCoercedNumber<unknown>>>>;
|
|
12
|
+
timeout: z.ZodOptional<z.ZodPreprocess<z.ZodNullable<z.ZodCoercedNumber<unknown>>>>;
|
|
13
13
|
provider: z.ZodOptional<z.ZodEnum<{
|
|
14
14
|
exa: "exa";
|
|
15
15
|
duckduckgo: "duckduckgo";
|
|
@@ -28,8 +28,8 @@ export declare const createWebSearchTool: () => Promise<{
|
|
|
28
28
|
description: string;
|
|
29
29
|
inputSchema: z.ZodObject<{
|
|
30
30
|
query: z.ZodString;
|
|
31
|
-
numResults: z.ZodOptional<z.
|
|
32
|
-
timeout: z.ZodOptional<z.
|
|
31
|
+
numResults: z.ZodOptional<z.ZodPreprocess<z.ZodNullable<z.ZodCoercedNumber<unknown>>>>;
|
|
32
|
+
timeout: z.ZodOptional<z.ZodPreprocess<z.ZodNullable<z.ZodCoercedNumber<unknown>>>>;
|
|
33
33
|
provider: z.ZodOptional<z.ZodEnum<{
|
|
34
34
|
exa: "exa";
|
|
35
35
|
duckduckgo: "duckduckgo";
|
package/dist/tools/web-search.js
CHANGED
|
@@ -217,7 +217,7 @@ export const createWebSearchTool = async () => {
|
|
|
217
217
|
inputSchema,
|
|
218
218
|
},
|
|
219
219
|
display({ query }) {
|
|
220
|
-
return
|
|
220
|
+
return `${style.cyan(query)}`;
|
|
221
221
|
},
|
|
222
222
|
async execute(options, executionOptions) {
|
|
223
223
|
return executeWebSearch(options, executionOptions);
|
|
@@ -8,7 +8,6 @@ type State = {
|
|
|
8
8
|
currentContextWindow: number;
|
|
9
9
|
contextWindow: number;
|
|
10
10
|
agentState?: AgentState;
|
|
11
|
-
currentMode?: string;
|
|
12
11
|
};
|
|
13
12
|
export declare class FooterComponent implements Component {
|
|
14
13
|
private modelManager;
|
|
@@ -16,7 +15,6 @@ export declare class FooterComponent implements Component {
|
|
|
16
15
|
private state;
|
|
17
16
|
private progressBar;
|
|
18
17
|
private agentState?;
|
|
19
|
-
private currentMode;
|
|
20
18
|
constructor(modelManager: ModelManager, tokenTracker: TokenTracker | undefined, state: State);
|
|
21
19
|
setState(state: State): void;
|
|
22
20
|
resetState(): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"footer.d.ts","sourceRoot":"","sources":["../../../source/tui/components/footer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AAGtE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAE5D,OAAO,EAAE,KAAK,SAAS,EAAgB,MAAM,WAAW,CAAC;AAGzD,KAAK,KAAK,GAAG;IACX,aAAa,EAAE,iBAAiB,CAAC;IACjC,oBAAoB,EAAE,MAAM,CAAC;IAC7B,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"footer.d.ts","sourceRoot":"","sources":["../../../source/tui/components/footer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AAGtE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAE5D,OAAO,EAAE,KAAK,SAAS,EAAgB,MAAM,WAAW,CAAC;AAGzD,KAAK,KAAK,GAAG;IACX,aAAa,EAAE,iBAAiB,CAAC;IACjC,oBAAoB,EAAE,MAAM,CAAC;IAC7B,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB,CAAC;AA2CF,qBAAa,eAAgB,YAAW,SAAS;IAC/C,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,YAAY,CAAC,CAAe;IACpC,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,UAAU,CAAC,CAAa;gBAE9B,YAAY,EAAE,YAAY,EAC1B,YAAY,EAAE,YAAY,GAAG,SAAS,EACtC,KAAK,EAAE,KAAK;IAad,QAAQ,CAAC,KAAK,EAAE,KAAK;IASrB,UAAU;IAIV,gBAAgB,IAAI,iBAAiB;IAIrC,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE;CAkEhC"}
|
|
@@ -41,7 +41,6 @@ export class FooterComponent {
|
|
|
41
41
|
state;
|
|
42
42
|
progressBar;
|
|
43
43
|
agentState;
|
|
44
|
-
currentMode = "Normal";
|
|
45
44
|
constructor(modelManager, tokenTracker, state) {
|
|
46
45
|
this.modelManager = modelManager;
|
|
47
46
|
this.tokenTracker = tokenTracker;
|
|
@@ -53,9 +52,6 @@ export class FooterComponent {
|
|
|
53
52
|
if (state.agentState) {
|
|
54
53
|
this.agentState = state.agentState;
|
|
55
54
|
}
|
|
56
|
-
if (state.currentMode !== undefined) {
|
|
57
|
-
this.currentMode = state.currentMode;
|
|
58
|
-
}
|
|
59
55
|
this.state = state;
|
|
60
56
|
this.progressBar.setCurrent(state.currentContextWindow);
|
|
61
57
|
this.progressBar.setTotal(state.contextWindow);
|
|
@@ -72,20 +68,8 @@ export class FooterComponent {
|
|
|
72
68
|
const [pathLine, gitLine] = formatProjectStatus(this.state.projectStatus);
|
|
73
69
|
const padding = Math.max(0, width - visibleWidth(pathLine) - modelInfo.length);
|
|
74
70
|
results.push(pathLine + " ".repeat(padding) + style.dim(modelInfo));
|
|
75
|
-
const modeDisplay = this.currentMode !== "Normal"
|
|
76
|
-
? style.magenta(`[${this.currentMode}]`)
|
|
77
|
-
: "";
|
|
78
71
|
if (gitLine) {
|
|
79
|
-
|
|
80
|
-
const gitPadding = Math.max(0, width - visibleWidth(gitLine) - visibleWidth(modeDisplay));
|
|
81
|
-
results.push(`${gitLine}${" ".repeat(gitPadding)}${modeDisplay}`);
|
|
82
|
-
}
|
|
83
|
-
else {
|
|
84
|
-
results.push(gitLine);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
else if (modeDisplay) {
|
|
88
|
-
results.push(modeDisplay);
|
|
72
|
+
results.push(gitLine);
|
|
89
73
|
}
|
|
90
74
|
// Line 3: Total session usage from token tracker (accumulated across all turns)
|
|
91
75
|
if (this.tokenTracker) {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Binary output handling for Bash tool
|
|
3
|
+
*
|
|
4
|
+
* Detects binary output from commands and saves it to temp files
|
|
5
|
+
* with helpful metadata for the user.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Check if output appears to be binary data
|
|
9
|
+
* Binary is detected by:
|
|
10
|
+
* - Null bytes (most reliable indicator)
|
|
11
|
+
* - High ratio of non-printable characters
|
|
12
|
+
*/
|
|
13
|
+
export declare function isBinaryOutput(output: string): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Result of saving binary output
|
|
16
|
+
*/
|
|
17
|
+
export interface BinarySaveResult {
|
|
18
|
+
success: boolean;
|
|
19
|
+
path?: string;
|
|
20
|
+
size?: number;
|
|
21
|
+
mimeType?: string;
|
|
22
|
+
error?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Save binary output to a temp file and detect its MIME type
|
|
26
|
+
*/
|
|
27
|
+
export declare function saveBinaryOutput(output: string): BinarySaveResult;
|
|
28
|
+
/**
|
|
29
|
+
* Format a user-friendly message for binary output
|
|
30
|
+
*/
|
|
31
|
+
export declare function formatBinaryMessage(result: BinarySaveResult): string;
|
|
32
|
+
//# sourceMappingURL=binary-output.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"binary-output.d.ts","sourceRoot":"","sources":["../../source/utils/binary-output.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAYH;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CA4BtD;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,CA4CjE;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,CAqBpE"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Binary output handling for Bash tool
|
|
3
|
+
*
|
|
4
|
+
* Detects binary output from commands and saves it to temp files
|
|
5
|
+
* with helpful metadata for the user.
|
|
6
|
+
*/
|
|
7
|
+
import { execSync } from "node:child_process";
|
|
8
|
+
import { randomBytes } from "node:crypto";
|
|
9
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { dirname } from "node:path";
|
|
11
|
+
/**
|
|
12
|
+
* Threshold for checking binary content (check first N bytes)
|
|
13
|
+
*/
|
|
14
|
+
const BINARY_CHECK_BYTES = 8192;
|
|
15
|
+
/**
|
|
16
|
+
* Check if output appears to be binary data
|
|
17
|
+
* Binary is detected by:
|
|
18
|
+
* - Null bytes (most reliable indicator)
|
|
19
|
+
* - High ratio of non-printable characters
|
|
20
|
+
*/
|
|
21
|
+
export function isBinaryOutput(output) {
|
|
22
|
+
if (output.length === 0) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
// Check first N bytes for null bytes (strongest binary indicator)
|
|
26
|
+
const checkLength = Math.min(output.length, BINARY_CHECK_BYTES);
|
|
27
|
+
const sample = output.slice(0, checkLength);
|
|
28
|
+
// Null byte is definitive binary indicator
|
|
29
|
+
if (sample.includes("\0")) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
// Count non-printable characters (excluding common whitespace)
|
|
33
|
+
let nonPrintable = 0;
|
|
34
|
+
for (let i = 0; i < sample.length; i++) {
|
|
35
|
+
const code = sample.charCodeAt(i);
|
|
36
|
+
// Allow printable ASCII (32-126), newlines (10), tabs (9), and carriage returns (13)
|
|
37
|
+
// Also allow extended ASCII/UTF-8 (127+)
|
|
38
|
+
if (code < 32 && code !== 9 && code !== 10 && code !== 13) {
|
|
39
|
+
nonPrintable++;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// If more than 30% non-printable in the sample, treat as binary
|
|
43
|
+
const ratio = nonPrintable / sample.length;
|
|
44
|
+
return ratio > 0.3;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Save binary output to a temp file and detect its MIME type
|
|
48
|
+
*/
|
|
49
|
+
export function saveBinaryOutput(output) {
|
|
50
|
+
try {
|
|
51
|
+
// Generate unique filename
|
|
52
|
+
const id = randomBytes(8).toString("hex");
|
|
53
|
+
const filePath = `/tmp/acai/bash_binary_${id}`;
|
|
54
|
+
// Ensure directory exists
|
|
55
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
56
|
+
// Convert string back to buffer for accurate binary writing
|
|
57
|
+
// Note: Some data loss may have occurred during UTF-8 decoding
|
|
58
|
+
const buffer = Buffer.from(output, "utf8");
|
|
59
|
+
writeFileSync(filePath, buffer);
|
|
60
|
+
const size = buffer.length;
|
|
61
|
+
// Detect MIME type using `file` command
|
|
62
|
+
let mimeType = "application/octet-stream";
|
|
63
|
+
try {
|
|
64
|
+
const fileOutput = execSync(`file --mime-type -b "${filePath}"`, {
|
|
65
|
+
encoding: "utf8",
|
|
66
|
+
timeout: 5000,
|
|
67
|
+
}).trim();
|
|
68
|
+
if (fileOutput && fileOutput !== "cannot open") {
|
|
69
|
+
mimeType = fileOutput;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// `file` command not available or failed, use default
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
success: true,
|
|
77
|
+
path: filePath,
|
|
78
|
+
size,
|
|
79
|
+
mimeType,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
84
|
+
return {
|
|
85
|
+
success: false,
|
|
86
|
+
error: errorMessage,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Format a user-friendly message for binary output
|
|
92
|
+
*/
|
|
93
|
+
export function formatBinaryMessage(result) {
|
|
94
|
+
if (!result.success) {
|
|
95
|
+
return `⚠️ Binary output detected but could not be saved: ${result.error ?? "Unknown error"}`;
|
|
96
|
+
}
|
|
97
|
+
const sizeStr = formatBytes(result.size ?? 0);
|
|
98
|
+
const lines = [
|
|
99
|
+
"📦 Binary output detected",
|
|
100
|
+
"",
|
|
101
|
+
`**Size:** ${sizeStr}`,
|
|
102
|
+
`**Type:** ${result.mimeType}`,
|
|
103
|
+
`**Saved to:** \`${result.path}\``,
|
|
104
|
+
"",
|
|
105
|
+
"**To inspect this file, you can use:**",
|
|
106
|
+
" • `file <path>` - Detect file type",
|
|
107
|
+
" • `xxd <path>` - Hex dump",
|
|
108
|
+
" • `hexdump -C <path>` - Hex dump with ASCII",
|
|
109
|
+
" • `head -c 100 <path> | xxd` - Preview first 100 bytes",
|
|
110
|
+
];
|
|
111
|
+
return lines.join("\n");
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Format bytes as human-readable string
|
|
115
|
+
*/
|
|
116
|
+
function formatBytes(bytes) {
|
|
117
|
+
if (bytes === 0)
|
|
118
|
+
return "0 bytes";
|
|
119
|
+
const units = ["bytes", "KB", "MB", "GB"];
|
|
120
|
+
const unitIndex = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
121
|
+
const size = bytes / 1024 ** unitIndex;
|
|
122
|
+
// Show decimal for KB and up, whole number for bytes
|
|
123
|
+
if (unitIndex === 0) {
|
|
124
|
+
return `${bytes} bytes`;
|
|
125
|
+
}
|
|
126
|
+
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
|
127
|
+
}
|
|
@@ -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,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,KAAK,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,MAAM,mBAAmB,GAAG,oBAAoB,GAAG,iBAAiB,CAAC;
|
|
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,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,KAAK,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,MAAM,mBAAmB,GAAG,oBAAoB,GAAG,iBAAiB,CAAC;AA0E3E;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,mBAAmB,CA4B7E;AA8WD;;GAEG;AACH,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,oBAAoB,GAC3B,MAAM,CAQR"}
|
|
@@ -2,6 +2,66 @@
|
|
|
2
2
|
* Command Protection Module
|
|
3
3
|
* Detects and blocks destructive commands that could cause data loss
|
|
4
4
|
*/
|
|
5
|
+
/**
|
|
6
|
+
* Check if a git subcommand appears as an actual command (not inside quoted strings).
|
|
7
|
+
* This helps avoid false positives when destructive command text appears in
|
|
8
|
+
* commit messages or other quoted strings.
|
|
9
|
+
*/
|
|
10
|
+
function isActualGitCommand(command, subcommand) {
|
|
11
|
+
const pattern = new RegExp(`\\bgit\\s+${subcommand}\\b`, "gi");
|
|
12
|
+
// Find all matches of git <subcommand>
|
|
13
|
+
const matches = [];
|
|
14
|
+
let match = pattern.exec(command);
|
|
15
|
+
while (match !== null) {
|
|
16
|
+
matches.push({ index: match.index, text: match[0] });
|
|
17
|
+
match = pattern.exec(command);
|
|
18
|
+
}
|
|
19
|
+
if (matches.length === 0)
|
|
20
|
+
return false;
|
|
21
|
+
// For each match, check if it's inside quotes
|
|
22
|
+
for (const { index } of matches) {
|
|
23
|
+
if (!isInsideQuotes(command, index)) {
|
|
24
|
+
// Also check if it's at the start or after command separators
|
|
25
|
+
const beforeMatch = command.slice(0, index).trim();
|
|
26
|
+
if (beforeMatch === "" ||
|
|
27
|
+
beforeMatch.endsWith("&&") ||
|
|
28
|
+
beforeMatch.endsWith("||") ||
|
|
29
|
+
beforeMatch.endsWith(";") ||
|
|
30
|
+
beforeMatch.endsWith("|") ||
|
|
31
|
+
beforeMatch.endsWith("\n")) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Check if a position in a string is inside quotes (single or double).
|
|
40
|
+
* Handles escaped quotes.
|
|
41
|
+
*/
|
|
42
|
+
function isInsideQuotes(command, position) {
|
|
43
|
+
let inSingleQuote = false;
|
|
44
|
+
let inDoubleQuote = false;
|
|
45
|
+
let escaped = false;
|
|
46
|
+
for (let i = 0; i < position; i++) {
|
|
47
|
+
const char = command[i];
|
|
48
|
+
if (escaped) {
|
|
49
|
+
escaped = false;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (char === "\\") {
|
|
53
|
+
escaped = true;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (char === '"' && !inSingleQuote) {
|
|
57
|
+
inDoubleQuote = !inDoubleQuote;
|
|
58
|
+
}
|
|
59
|
+
else if (char === "'" && !inDoubleQuote) {
|
|
60
|
+
inSingleQuote = !inSingleQuote;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return inSingleQuote || inDoubleQuote;
|
|
64
|
+
}
|
|
5
65
|
/**
|
|
6
66
|
* Detects if a command is destructive and should be blocked
|
|
7
67
|
* @param command - The full command string to check
|
|
@@ -91,15 +151,19 @@ function detectDestructiveGitCommands(command) {
|
|
|
91
151
|
};
|
|
92
152
|
}
|
|
93
153
|
// Block git branch -D (force delete without merge check)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
154
|
+
// Check for uppercase flag on original command (lowercase -d is safe)
|
|
155
|
+
// Match 'git' case-insensitively, but preserve case for the flag
|
|
156
|
+
// Only match if git branch appears as an actual command (not inside quoted strings)
|
|
157
|
+
const branchMatch = lowerCommand.match(/git\s+branch\s+-([a-z])/);
|
|
158
|
+
if (branchMatch) {
|
|
159
|
+
// Check if the flag in the original command is uppercase
|
|
160
|
+
const flagInOriginal = command.match(/git\s+branch\s+-([A-Za-z])/i);
|
|
161
|
+
if (flagInOriginal &&
|
|
162
|
+
flagInOriginal[1] === flagInOriginal[1].toUpperCase()) {
|
|
163
|
+
// Verify this is an actual git branch command, not text inside quotes
|
|
164
|
+
// Check if git branch appears at start or after command separators
|
|
165
|
+
const isActualCommand = isActualGitCommand(command, "branch");
|
|
166
|
+
if (isActualCommand) {
|
|
103
167
|
return {
|
|
104
168
|
blocked: true,
|
|
105
169
|
reason: "git branch -D force-deletes branches without checking if they're merged",
|
|
@@ -216,8 +280,27 @@ function detectDangerousInlineScripts(command) {
|
|
|
216
280
|
}
|
|
217
281
|
/**
|
|
218
282
|
* Detect dangerous patterns in heredocs and here-strings
|
|
283
|
+
* Only blocks heredocs that are explicitly executed by a scripting language,
|
|
284
|
+
* not heredocs used as data (e.g., commit messages, config files).
|
|
219
285
|
*/
|
|
220
286
|
function detectDangerousHeredocs(command) {
|
|
287
|
+
// Check if heredoc is being executed by a scripting language
|
|
288
|
+
// Patterns that indicate execution:
|
|
289
|
+
// 1. bash <<EOF, sh <<EOF, python <<EOF, etc. (language reads from heredoc)
|
|
290
|
+
// 2. cat <<EOF | bash, <<EOF | python, etc. (heredoc piped to language)
|
|
291
|
+
const executionPatterns = [
|
|
292
|
+
// Shell languages reading heredoc directly
|
|
293
|
+
/\b(bash|sh|zsh|dash|ksh)\s*<<-?\s*['"]?\w+/i,
|
|
294
|
+
// Scripting languages reading heredoc directly
|
|
295
|
+
/\b(python\d?|ruby|perl|node)\s*<<-?\s*['"]?\w+/i,
|
|
296
|
+
// Heredoc piped to shell
|
|
297
|
+
/<<-?\s*['"]?\w+['"]?\s*\|\s*(bash|sh|zsh|dash|ksh)\b/i,
|
|
298
|
+
];
|
|
299
|
+
const isExecutableHeredoc = executionPatterns.some((pattern) => pattern.test(command));
|
|
300
|
+
// Only scan heredoc content if it's being executed by a scripting language
|
|
301
|
+
if (!isExecutableHeredoc) {
|
|
302
|
+
return { blocked: false };
|
|
303
|
+
}
|
|
221
304
|
// Match heredoc patterns: <<EOF ... EOF
|
|
222
305
|
const heredocPattern = /<<-?\s*['"]?(\w+)['"]?\s*([\s\S]*?)\n\1\b/gi;
|
|
223
306
|
let match = null;
|
package/dist/utils/parsing.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parsing.d.ts","sourceRoot":"","sources":["../../source/utils/parsing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,CAAC,EAAE,MAAM,KAAK,CAAC;AAiBtC,wBAAgB,UAAU,CAAC,CAAC,SAAS,CAAC,CAAC,UAAU,EAAE,KAAK,EAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"parsing.d.ts","sourceRoot":"","sources":["../../source/utils/parsing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,CAAC,EAAE,MAAM,KAAK,CAAC;AAiBtC,wBAAgB,UAAU,CAAC,CAAC,SAAS,CAAC,CAAC,UAAU,EAAE,KAAK,EAAE,CAAC,sBAE1D"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travisennis/acai",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
4
4
|
"description": "An AI assistant for developing software.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"lint:ast-grep:fix": "sg scan --update-all",
|
|
34
34
|
"lint:biome": "biome lint",
|
|
35
35
|
"lint:biome:fix": "biome lint --unsafe --write",
|
|
36
|
-
"lint:fix": "
|
|
36
|
+
"lint:fix": "npm run lint:ast-grep && npm run lint:biome:fix",
|
|
37
37
|
"lint:staged": "biome lint --error-on-warnings --no-errors-on-unmatched --staged",
|
|
38
38
|
"format:staged": "biome check --staged --formatter-enabled=true --linter-enabled=false --no-errors-on-unmatched",
|
|
39
39
|
"prepack": "npm run clean && npm run build",
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"oxlint:single": "oxlint --type-aware --ignore-path .gitignore -A all -c .oxlintrc.json",
|
|
50
50
|
"knip": "npx knip",
|
|
51
51
|
"knip:prod": "npx knip --production",
|
|
52
|
+
"setup": "node scripts/setup.ts",
|
|
52
53
|
"update": "npx npm-check-updates --interactive --format group",
|
|
53
54
|
"cpd": "npx jscpd ./source",
|
|
54
55
|
"typecheck": "tsc --noEmit --pretty -p tsconfig.json",
|
|
@@ -57,45 +58,46 @@
|
|
|
57
58
|
"lint:length": "find source -name '*.ts' | xargs -I{} awk 'END{if(NR>500)print FILENAME\": \"NR\" lines\"}' {}"
|
|
58
59
|
},
|
|
59
60
|
"dependencies": {
|
|
60
|
-
"@ai-sdk/
|
|
61
|
-
"@ai-sdk/
|
|
62
|
-
"@ai-sdk/
|
|
63
|
-
"@ai-sdk/
|
|
64
|
-
"@ai-sdk/
|
|
65
|
-
"@ai-sdk/
|
|
66
|
-
"@ai-sdk/
|
|
67
|
-
"@ai-sdk/openai
|
|
61
|
+
"@ai-sdk/alibaba": "^1.0.21",
|
|
62
|
+
"@ai-sdk/anthropic": "^3.0.74",
|
|
63
|
+
"@ai-sdk/deepseek": "^2.0.32",
|
|
64
|
+
"@ai-sdk/devtools": "^0.0.17",
|
|
65
|
+
"@ai-sdk/google": "^3.0.67",
|
|
66
|
+
"@ai-sdk/groq": "^3.0.38",
|
|
67
|
+
"@ai-sdk/open-responses": "^1.0.14",
|
|
68
|
+
"@ai-sdk/openai": "^3.0.58",
|
|
69
|
+
"@ai-sdk/openai-compatible": "^2.0.45",
|
|
68
70
|
"@crosscopy/clipboard": "^0.3.6",
|
|
69
71
|
"@travisennis/stdlib": "^0.0.14",
|
|
70
|
-
"ai": "^6.0.
|
|
72
|
+
"ai": "^6.0.174",
|
|
71
73
|
"cheerio": "^1.2.0",
|
|
72
|
-
"diff": "^
|
|
74
|
+
"diff": "^9.0.0",
|
|
73
75
|
"fast-glob": "^3.3.3",
|
|
74
76
|
"fdir": "^6.5.0",
|
|
75
77
|
"highlight.js": "^11.11.1",
|
|
76
|
-
"marked": "
|
|
78
|
+
"marked": "18.0.3",
|
|
77
79
|
"p-throttle": "^8.1.0",
|
|
78
|
-
"parse5": "^8.0.
|
|
79
|
-
"parse5-htmlparser2-tree-adapter": "^8.0.
|
|
80
|
+
"parse5": "^8.0.1",
|
|
81
|
+
"parse5-htmlparser2-tree-adapter": "^8.0.1",
|
|
80
82
|
"pino": "^10.3.1",
|
|
81
83
|
"pino-pretty": "^13.1.3",
|
|
82
84
|
"pino-roll": "^4.0.0",
|
|
83
85
|
"tiktoken": "^1.0.22",
|
|
84
|
-
"zod": "^4.
|
|
86
|
+
"zod": "^4.4.2"
|
|
85
87
|
},
|
|
86
88
|
"devDependencies": {
|
|
87
|
-
"@ai-sdk/provider": "^3.0.
|
|
88
|
-
"@ast-grep/napi": "^0.42.
|
|
89
|
-
"@biomejs/biome": "2.4.
|
|
90
|
-
"@commitlint/config-conventional": "^20.5.
|
|
91
|
-
"@types/node": "^25.
|
|
89
|
+
"@ai-sdk/provider": "^3.0.10",
|
|
90
|
+
"@ast-grep/napi": "^0.42.1",
|
|
91
|
+
"@biomejs/biome": "2.4.14",
|
|
92
|
+
"@commitlint/config-conventional": "^20.5.3",
|
|
93
|
+
"@types/node": "^25.6.0",
|
|
92
94
|
"c8": "^11.0.0",
|
|
93
|
-
"commitlint": "^20.5.
|
|
95
|
+
"commitlint": "^20.5.3",
|
|
94
96
|
"domhandler": "^6.0.1",
|
|
95
97
|
"husky": "^9.1.7",
|
|
96
|
-
"oxlint": "^1.
|
|
97
|
-
"oxlint-tsgolint": "^0.
|
|
98
|
-
"typescript": "^
|
|
98
|
+
"oxlint": "^1.62.0",
|
|
99
|
+
"oxlint-tsgolint": "^0.22.1",
|
|
100
|
+
"typescript": "^6.0.3"
|
|
99
101
|
},
|
|
100
102
|
"engines": {
|
|
101
103
|
"node": ">=20"
|
package/dist/modes/manager.d.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import type { UserModelMessage } from "ai";
|
|
2
|
-
type Mode = "normal" | "planning" | "research";
|
|
3
|
-
export declare class ModeManager {
|
|
4
|
-
private currentMode;
|
|
5
|
-
private firstMessageInMode;
|
|
6
|
-
getCurrentMode(): Mode;
|
|
7
|
-
getDisplayName(): string;
|
|
8
|
-
cycleMode(): void;
|
|
9
|
-
getInitialPrompt(): string;
|
|
10
|
-
getReminderPrompt(): string;
|
|
11
|
-
isNormal(): boolean;
|
|
12
|
-
isFirstMessage(): boolean;
|
|
13
|
-
markFirstMessageSent(): void;
|
|
14
|
-
getReminderMessage(): UserModelMessage | undefined;
|
|
15
|
-
reset(): void;
|
|
16
|
-
toJson(): {
|
|
17
|
-
mode: Mode;
|
|
18
|
-
};
|
|
19
|
-
fromJson(data: {
|
|
20
|
-
mode?: string;
|
|
21
|
-
}): void;
|
|
22
|
-
}
|
|
23
|
-
export {};
|
|
24
|
-
//# sourceMappingURL=manager.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../source/modes/manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,IAAI,CAAC;AAI3C,KAAK,IAAI,GAAG,QAAQ,GAAG,UAAU,GAAG,UAAU,CAAC;AAmC/C,qBAAa,WAAW;IACtB,OAAO,CAAC,WAAW,CAAkB;IACrC,OAAO,CAAC,kBAAkB,CAAQ;IAElC,cAAc,IAAI,IAAI;IAItB,cAAc,IAAI,MAAM;IAIxB,SAAS,IAAI,IAAI;IAOjB,gBAAgB,IAAI,MAAM;IAI1B,iBAAiB,IAAI,MAAM;IAI3B,QAAQ,IAAI,OAAO;IAInB,cAAc,IAAI,OAAO;IAIzB,oBAAoB,IAAI,IAAI;IAI5B,kBAAkB,IAAI,gBAAgB,GAAG,SAAS;IAWlD,KAAK,IAAI,IAAI;IAKb,MAAM,IAAI;QAAE,IAAI,EAAE,IAAI,CAAA;KAAE;IAIxB,QAAQ,CAAC,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;CAMxC"}
|