@ocis/myagent-cli 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/myagent.js +7 -2
- package/dist/agent/turn.js +5 -0
- package/dist/approval/policy.js +1 -1
- package/dist/index.js +6 -13
- package/dist/runtime.d.ts +7 -2
- package/dist/runtime.js +51 -40
- package/dist/tools/grep.js +5 -6
- package/dist/ui/components.d.ts +4 -0
- package/dist/ui/components.js +27 -3
- package/package.json +1 -1
package/bin/myagent.js
CHANGED
|
@@ -21,13 +21,18 @@ for (const sig of ["SIGTERM", "SIGHUP"]) {
|
|
|
21
21
|
process.on(sig, () => { try { child?.kill(sig); } catch { /* already gone */ } });
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/** pi-tui's regex needs the `v` flag, which Node only has from 22.19. */
|
|
25
|
+
function nodeTooOld() {
|
|
26
|
+
const [major, minor] = process.versions.node.split(".").map(Number);
|
|
27
|
+
return major < 22 || (major === 22 && minor < 19);
|
|
28
|
+
}
|
|
29
|
+
|
|
24
30
|
function launch(runtime) {
|
|
25
31
|
child = spawn(runtime, [entry, ...args], { stdio: "inherit" });
|
|
26
32
|
|
|
27
33
|
child.once("error", (err) => {
|
|
28
34
|
if (err.code === "ENOENT" && runtime !== process.execPath) {
|
|
29
|
-
|
|
30
|
-
if (major < 22 || (major === 22 && minor < 19)) {
|
|
35
|
+
if (nodeTooOld()) {
|
|
31
36
|
console.error(
|
|
32
37
|
`myagent requires Bun or Node.js >= 22.19 (found Node ${process.versions.node}).\n` +
|
|
33
38
|
"Install Bun: https://bun.sh",
|
package/dist/agent/turn.js
CHANGED
|
@@ -850,6 +850,11 @@ export class AgentRunner {
|
|
|
850
850
|
if (adopted) {
|
|
851
851
|
live.push({ ...output, tool_call_id: adopted.id });
|
|
852
852
|
unmatched.delete(adopted.id);
|
|
853
|
+
// The original id is never submitted (its result was re-attached to
|
|
854
|
+
// the retry), so it must leave the pending set — otherwise the run-end
|
|
855
|
+
// sweep announces a release for a call that was answered.
|
|
856
|
+
if (call)
|
|
857
|
+
this.pendingSubmission.delete(call.id);
|
|
853
858
|
cb.onNotice(`Re-attached the result of ${call?.name ?? "a tool call"} to the agent's retry.`, "info");
|
|
854
859
|
continue;
|
|
855
860
|
}
|
package/dist/approval/policy.js
CHANGED
|
@@ -142,7 +142,7 @@ export class ApprovalPolicy {
|
|
|
142
142
|
try {
|
|
143
143
|
// A cancel/timeout while queued settles immediately — no card, no wait.
|
|
144
144
|
if (wait)
|
|
145
|
-
await Promise.race([previous
|
|
145
|
+
await Promise.race([previous, deadline.promise]);
|
|
146
146
|
if (settled)
|
|
147
147
|
return await deadline.promise;
|
|
148
148
|
return await Promise.race([this.prompt(request, controller.signal), deadline.promise]);
|
package/dist/index.js
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
import { createInterface } from "node:readline/promises";
|
|
9
9
|
import { stdin as input, stdout as output } from "node:process";
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
|
-
import { readFile } from "node:fs/promises";
|
|
12
11
|
import { resolve } from "node:path";
|
|
12
|
+
import pkg from "../package.json" with { type: "json" };
|
|
13
13
|
import { IntegrationClient } from "./protocol/client.js";
|
|
14
14
|
import { ToolRegistry } from "./tools/registry.js";
|
|
15
15
|
import { combinedIntegrationUrl, configDir, isConfigured, loadConfig, migrateLegacyConfigDir, resolveConfig, saveConfig, sessionsPath, splitIntegrationUrl, } from "./config.js";
|
|
@@ -21,18 +21,11 @@ import { discoverSkills, formatSkillList, resolveSkillDirs } from "./skills/disc
|
|
|
21
21
|
import { sanitizeForTerminal } from "./sanitize.js";
|
|
22
22
|
import { isMainModule, readStdinText } from "./runtime.js";
|
|
23
23
|
/**
|
|
24
|
-
* Package version,
|
|
25
|
-
*
|
|
24
|
+
* Package version, bundled from package.json so npm releases and the compiled
|
|
25
|
+
* binary never drift from `--version` (a runtime read cannot resolve inside
|
|
26
|
+
* `bun build --compile`'s virtual filesystem).
|
|
26
27
|
*/
|
|
27
|
-
|
|
28
|
-
try {
|
|
29
|
-
const pkg = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
|
|
30
|
-
return pkg.version ?? "0.0.0";
|
|
31
|
-
}
|
|
32
|
-
catch {
|
|
33
|
-
return "0.0.0";
|
|
34
|
-
}
|
|
35
|
-
}
|
|
28
|
+
const VERSION = pkg.version;
|
|
36
29
|
const HELP = `myagent — a coding agent for your local repository, powered by MyAgent
|
|
37
30
|
|
|
38
31
|
Usage:
|
|
@@ -304,7 +297,7 @@ async function main() {
|
|
|
304
297
|
return 0;
|
|
305
298
|
}
|
|
306
299
|
if (args.command === "version") {
|
|
307
|
-
process.stdout.write(`${
|
|
300
|
+
process.stdout.write(`${VERSION}\n`);
|
|
308
301
|
return 0;
|
|
309
302
|
}
|
|
310
303
|
const stored = await loadConfig();
|
package/dist/runtime.d.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
/** Resolve an executable on PATH (Bun.which on Bun, a PATH scan on Node). */
|
|
2
|
-
export declare function which(cmd: string): string | null;
|
|
3
1
|
/** Read all of stdin as UTF-8 text. Callers check `process.stdin.isTTY` first. */
|
|
4
2
|
export declare function readStdinText(): Promise<string>;
|
|
5
3
|
/**
|
|
@@ -24,3 +22,10 @@ export interface SpawnOptions {
|
|
|
24
22
|
detached?: boolean;
|
|
25
23
|
}
|
|
26
24
|
export declare function spawnProcess(args: string[], opts?: SpawnOptions): ProcessHandle;
|
|
25
|
+
/**
|
|
26
|
+
* Exit code for a child that ended, matching Bun's convention: a signal death
|
|
27
|
+
* is 128 + signal number (SIGKILL → 137). Node's `close` reports `code: null`
|
|
28
|
+
* with the signal name, so without this the same kill reads as 0 on Node and
|
|
29
|
+
* 137 on Bun — abort paths key on the code.
|
|
30
|
+
*/
|
|
31
|
+
export declare function exitCodeFromClose(code: number | null, signal: NodeJS.Signals | null): number;
|
package/dist/runtime.js
CHANGED
|
@@ -2,40 +2,18 @@
|
|
|
2
2
|
// Runtime shims: the few places where Bun and Node genuinely differ.
|
|
3
3
|
//
|
|
4
4
|
// Everything else uses node: builtins, which Bun implements natively — only
|
|
5
|
-
// subprocess spawning (Bun.spawn vs node:child_process),
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
5
|
+
// subprocess spawning (Bun.spawn vs node:child_process), stdin (Bun.stdin) and
|
|
6
|
+
// entry-point detection (import.meta.main) need a runtime branch. The Bun
|
|
7
|
+
// branches are guarded by `typeof Bun`, so the emitted JS never touches the
|
|
8
|
+
// `Bun` global on Node.
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
10
|
import { spawn } from "node:child_process";
|
|
11
|
-
import { realpathSync
|
|
11
|
+
import { realpathSync } from "node:fs";
|
|
12
12
|
import { Buffer } from "node:buffer";
|
|
13
|
-
import {
|
|
13
|
+
import { constants as osConstants } from "node:os";
|
|
14
14
|
import { Readable } from "node:stream";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
16
|
const isBun = typeof Bun !== "undefined";
|
|
17
|
-
// --- which -----------------------------------------------------------------
|
|
18
|
-
/** Resolve an executable on PATH (Bun.which on Bun, a PATH scan on Node). */
|
|
19
|
-
export function which(cmd) {
|
|
20
|
-
if (isBun && Bun.which)
|
|
21
|
-
return Bun.which(cmd);
|
|
22
|
-
const exts = process.platform === "win32"
|
|
23
|
-
? (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean)
|
|
24
|
-
: [""];
|
|
25
|
-
for (const dir of (process.env.PATH ?? "").split(delimiter)) {
|
|
26
|
-
for (const ext of exts) {
|
|
27
|
-
const candidate = join(dir, cmd + ext);
|
|
28
|
-
try {
|
|
29
|
-
if (statSync(candidate).isFile())
|
|
30
|
-
return candidate;
|
|
31
|
-
}
|
|
32
|
-
catch {
|
|
33
|
-
/* keep looking */
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
17
|
// --- stdin -----------------------------------------------------------------
|
|
40
18
|
/** Read all of stdin as UTF-8 text. Callers check `process.stdin.isTTY` first. */
|
|
41
19
|
export async function readStdinText() {
|
|
@@ -70,18 +48,51 @@ export function isMainModule(meta) {
|
|
|
70
48
|
export function spawnProcess(args, opts) {
|
|
71
49
|
return isBun ? spawnBun(args, opts) : spawnNode(args, opts);
|
|
72
50
|
}
|
|
51
|
+
/** An already-ended stream, for pipes that were not requested. */
|
|
52
|
+
function emptyStream() {
|
|
53
|
+
return new ReadableStream();
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Exit code for a child that ended, matching Bun's convention: a signal death
|
|
57
|
+
* is 128 + signal number (SIGKILL → 137). Node's `close` reports `code: null`
|
|
58
|
+
* with the signal name, so without this the same kill reads as 0 on Node and
|
|
59
|
+
* 137 on Bun — abort paths key on the code.
|
|
60
|
+
*/
|
|
61
|
+
export function exitCodeFromClose(code, signal) {
|
|
62
|
+
if (code !== null)
|
|
63
|
+
return code;
|
|
64
|
+
if (!signal)
|
|
65
|
+
return 0;
|
|
66
|
+
return 128 + (osConstants.signals[signal] ?? 0);
|
|
67
|
+
}
|
|
73
68
|
function spawnBun(args, opts) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
69
|
+
let proc;
|
|
70
|
+
try {
|
|
71
|
+
proc = Bun.spawn(args, {
|
|
72
|
+
cwd: opts?.cwd,
|
|
73
|
+
stdin: "ignore",
|
|
74
|
+
stdout: opts?.stdout === "pipe" ? "pipe" : "ignore",
|
|
75
|
+
stderr: opts?.stderr === "pipe" ? "pipe" : "ignore",
|
|
76
|
+
detached: opts?.detached,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
// Bun throws synchronously for a missing binary; Node reports it through
|
|
81
|
+
// `exited` = -1. Normalize so callers see one contract on both runtimes.
|
|
82
|
+
if (err?.code !== "ENOENT")
|
|
83
|
+
throw err;
|
|
84
|
+
return {
|
|
85
|
+
pid: 0,
|
|
86
|
+
stdout: emptyStream(),
|
|
87
|
+
stderr: emptyStream(),
|
|
88
|
+
exited: Promise.resolve(-1),
|
|
89
|
+
kill: () => { },
|
|
90
|
+
};
|
|
91
|
+
}
|
|
81
92
|
return {
|
|
82
93
|
pid: proc.pid,
|
|
83
|
-
stdout: proc.stdout,
|
|
84
|
-
stderr: proc.stderr,
|
|
94
|
+
stdout: opts?.stdout === "pipe" ? proc.stdout : emptyStream(),
|
|
95
|
+
stderr: opts?.stderr === "pipe" ? proc.stderr : emptyStream(),
|
|
85
96
|
exited: proc.exited,
|
|
86
97
|
kill: (signal) => { try {
|
|
87
98
|
proc.kill(signal);
|
|
@@ -101,17 +112,17 @@ function spawnNode(args, opts) {
|
|
|
101
112
|
});
|
|
102
113
|
// First of close/error wins: a spawn failure (ENOENT) may emit only `error`.
|
|
103
114
|
const exited = new Promise((resolve) => {
|
|
104
|
-
child.once("close", (code) => resolve(code
|
|
115
|
+
child.once("close", (code, signal) => resolve(exitCodeFromClose(code, signal)));
|
|
105
116
|
child.once("error", () => resolve(-1));
|
|
106
117
|
});
|
|
107
118
|
return {
|
|
108
119
|
pid: child.pid ?? 0,
|
|
109
120
|
stdout: child.stdout
|
|
110
121
|
? Readable.toWeb(child.stdout)
|
|
111
|
-
:
|
|
122
|
+
: emptyStream(),
|
|
112
123
|
stderr: child.stderr
|
|
113
124
|
? Readable.toWeb(child.stderr)
|
|
114
|
-
:
|
|
125
|
+
: emptyStream(),
|
|
115
126
|
exited,
|
|
116
127
|
kill: (signal) => { try {
|
|
117
128
|
child.kill(signal);
|
package/dist/tools/grep.js
CHANGED
|
@@ -8,7 +8,7 @@ import { readdir, readFile, stat } from "node:fs/promises";
|
|
|
8
8
|
import { join, relative } from "node:path";
|
|
9
9
|
import { defineTool, optionalString, requireString } from "./types.js";
|
|
10
10
|
import { resolveToolPath } from "./paths.js";
|
|
11
|
-
import { spawnProcess
|
|
11
|
+
import { spawnProcess } from "../runtime.js";
|
|
12
12
|
const MAX_MATCHES = 100;
|
|
13
13
|
/**
|
|
14
14
|
* Wall-clock budget for the JS fallback walk. The pattern is LLM-authored and
|
|
@@ -31,7 +31,9 @@ function formatMatches(matches, workspace) {
|
|
|
31
31
|
return lines.join("\n") + (truncated ? `\n… (results capped at ${MAX_MATCHES} matches)` : "");
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
34
|
-
* Search with ripgrep when available. Returns null when rg is unavailable
|
|
34
|
+
* Search with ripgrep when available. Returns null when rg is unavailable
|
|
35
|
+
* (missing binary, rejected pattern, spawn failure) so the caller falls back
|
|
36
|
+
* to the JS walk.
|
|
35
37
|
*
|
|
36
38
|
* The cap is enforced HERE, on the parse: rg's `--max-count` is per FILE, so
|
|
37
39
|
* on a large tree it can still produce a huge JSON stream. The output is read
|
|
@@ -39,9 +41,6 @@ function formatMatches(matches, workspace) {
|
|
|
39
41
|
* buffer nor the child outlives the cap.
|
|
40
42
|
*/
|
|
41
43
|
async function searchWithRipgrep(pattern, searchPath, include, signal) {
|
|
42
|
-
const rg = which("rg");
|
|
43
|
-
if (!rg)
|
|
44
|
-
return null;
|
|
45
44
|
// --max-count bounds the per-file work rg does; the global cap is ours.
|
|
46
45
|
const args = ["--json", "--max-count", String(MAX_MATCHES), "--no-ignore", "-e", pattern];
|
|
47
46
|
if (include)
|
|
@@ -50,7 +49,7 @@ async function searchWithRipgrep(pattern, searchPath, include, signal) {
|
|
|
50
49
|
args.push("--glob", `!**/${dir}/**`);
|
|
51
50
|
args.push(searchPath);
|
|
52
51
|
try {
|
|
53
|
-
const proc = spawnProcess([rg, ...args], { stdout: "pipe", stderr: "ignore" });
|
|
52
|
+
const proc = spawnProcess(["rg", ...args], { stdout: "pipe", stderr: "ignore" });
|
|
54
53
|
const onAbort = () => { try {
|
|
55
54
|
proc.kill();
|
|
56
55
|
}
|
package/dist/ui/components.d.ts
CHANGED
|
@@ -42,6 +42,9 @@ export declare class AssistantMessageComponent implements Component {
|
|
|
42
42
|
/** Raw stream (unsanitized) — the sanitizing pass needs the whole text. */
|
|
43
43
|
private raw;
|
|
44
44
|
private text;
|
|
45
|
+
/** Set once any delta carried a control char: from then on the accumulated
|
|
46
|
+
* text must be re-sanitized (a sequence can span deltas). */
|
|
47
|
+
private dirty;
|
|
45
48
|
constructor();
|
|
46
49
|
append(delta: string): void;
|
|
47
50
|
get content(): string;
|
|
@@ -51,6 +54,7 @@ export declare class AssistantMessageComponent implements Component {
|
|
|
51
54
|
export declare class ReasoningBlockComponent implements Component {
|
|
52
55
|
private raw;
|
|
53
56
|
private text;
|
|
57
|
+
private dirty;
|
|
54
58
|
expanded: boolean;
|
|
55
59
|
append(delta: string): void;
|
|
56
60
|
toggle(): void;
|
package/dist/ui/components.js
CHANGED
|
@@ -84,11 +84,21 @@ export class UserMessageComponent {
|
|
|
84
84
|
];
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Characters `sanitizeForTerminal` would change (ESC and C0/C1 controls; tab
|
|
89
|
+
* and newline are kept). While a stream has never contained one, the sanitizer
|
|
90
|
+
* is the identity — re-running it over the whole accumulated text on every
|
|
91
|
+
* delta is O(n²) for no effect.
|
|
92
|
+
*/
|
|
93
|
+
const CONTROL_CHARS = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/;
|
|
87
94
|
export class AssistantMessageComponent {
|
|
88
95
|
md;
|
|
89
96
|
/** Raw stream (unsanitized) — the sanitizing pass needs the whole text. */
|
|
90
97
|
raw = "";
|
|
91
98
|
text = "";
|
|
99
|
+
/** Set once any delta carried a control char: from then on the accumulated
|
|
100
|
+
* text must be re-sanitized (a sequence can span deltas). */
|
|
101
|
+
dirty = false;
|
|
92
102
|
constructor() {
|
|
93
103
|
this.md = new Markdown("", 0, 0, markdownTheme, { color: assistantColor });
|
|
94
104
|
}
|
|
@@ -97,9 +107,16 @@ export class AssistantMessageComponent {
|
|
|
97
107
|
// material (file bodies, fetched pages) verbatim — strip terminal
|
|
98
108
|
// sequences, like tool output and headless deltas do. Sanitizing the
|
|
99
109
|
// ACCUMULATED text, not the delta: a sequence split across two deltas
|
|
100
|
-
// would slip through a per-delta pass.
|
|
110
|
+
// would slip through a per-delta pass. The clean fast path is exact: with
|
|
111
|
+
// no control char anywhere, the sanitizer returns the input unchanged.
|
|
101
112
|
this.raw += delta;
|
|
102
|
-
this.
|
|
113
|
+
if (!this.dirty && !CONTROL_CHARS.test(delta)) {
|
|
114
|
+
this.text = this.raw;
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
this.dirty = true;
|
|
118
|
+
this.text = sanitizeForTerminal(this.raw);
|
|
119
|
+
}
|
|
103
120
|
this.md.setText(this.text);
|
|
104
121
|
}
|
|
105
122
|
get content() {
|
|
@@ -164,12 +181,19 @@ export class AssistantMessageComponent {
|
|
|
164
181
|
export class ReasoningBlockComponent {
|
|
165
182
|
raw = "";
|
|
166
183
|
text = "";
|
|
184
|
+
dirty = false;
|
|
167
185
|
expanded = false;
|
|
168
186
|
append(delta) {
|
|
169
187
|
// Same rule as assistant text: reasoning is model output rendered to the
|
|
170
188
|
// real terminal, and it quotes tool output freely.
|
|
171
189
|
this.raw += delta;
|
|
172
|
-
this.
|
|
190
|
+
if (!this.dirty && !CONTROL_CHARS.test(delta)) {
|
|
191
|
+
this.text = this.raw;
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
this.dirty = true;
|
|
195
|
+
this.text = sanitizeForTerminal(this.raw);
|
|
196
|
+
}
|
|
173
197
|
}
|
|
174
198
|
toggle() {
|
|
175
199
|
this.expanded = !this.expanded;
|