@ocis/myagent-cli 0.2.0 → 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/README.md +7 -7
- package/bin/myagent.js +52 -0
- package/dist/agent/sessions.js +2 -5
- package/dist/agent/turn.js +5 -0
- package/dist/approval/policy.js +1 -1
- package/dist/config.js +2 -6
- package/dist/git/status.js +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +7 -15
- package/dist/runtime.d.ts +7 -14
- package/dist/runtime.js +55 -89
- package/dist/skills/discovery.js +2 -3
- package/dist/tools/grep.js +9 -10
- package/dist/tools/shell.js +0 -2
- package/dist/ui/app.js +1 -1
- package/dist/ui/components.d.ts +4 -0
- package/dist/ui/components.js +27 -3
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -36,15 +36,15 @@ local repo (cwd) remote myagent
|
|
|
36
36
|
## Install
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
bunx @ocis/myagent-cli # one-off, no install
|
|
40
|
+
npm install -g @ocis/myagent-cli # global
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
The CLI runs on **
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
`
|
|
43
|
+
The CLI runs on **Bun ≥1.0** or **Node.js ≥22.19**. The published bin is a small
|
|
44
|
+
launcher that prefers Bun and falls back to Node, so `bunx` and `npm install -g`
|
|
45
|
+
both work with whichever runtime you have. Configure credentials with
|
|
46
|
+
`myagent config`, or set `MYAGENT_BASE_URL` / `MYAGENT_INTEGRATION_ID` /
|
|
47
|
+
`MYAGENT_API_KEY`.
|
|
48
48
|
|
|
49
49
|
## Plan / Coding / Cowork modes
|
|
50
50
|
|
package/bin/myagent.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Runtime launcher for the myagent CLI.
|
|
3
|
+
//
|
|
4
|
+
// dist/index.js runs on Bun or Node >= 22.19, but a bin shebang names exactly
|
|
5
|
+
// one interpreter — and `bunx` honors it, so a plain Node shebang would run
|
|
6
|
+
// the CLI under whatever Node happens to be on PATH (often too old for
|
|
7
|
+
// pi-tui). This wrapper prefers Bun and falls back to Node.
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
|
|
11
|
+
const entry = fileURLToPath(new URL("../dist/index.js", import.meta.url));
|
|
12
|
+
const args = process.argv.slice(2);
|
|
13
|
+
|
|
14
|
+
// Ctrl+C reaches the whole foreground process group, so the CLI handles it
|
|
15
|
+
// itself; the launcher must not exit before that shutdown finishes. SIGTERM /
|
|
16
|
+
// SIGHUP sent to the launcher PID alone are forwarded.
|
|
17
|
+
process.on("SIGINT", () => {});
|
|
18
|
+
|
|
19
|
+
let child;
|
|
20
|
+
for (const sig of ["SIGTERM", "SIGHUP"]) {
|
|
21
|
+
process.on(sig, () => { try { child?.kill(sig); } catch { /* already gone */ } });
|
|
22
|
+
}
|
|
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
|
+
|
|
30
|
+
function launch(runtime) {
|
|
31
|
+
child = spawn(runtime, [entry, ...args], { stdio: "inherit" });
|
|
32
|
+
|
|
33
|
+
child.once("error", (err) => {
|
|
34
|
+
if (err.code === "ENOENT" && runtime !== process.execPath) {
|
|
35
|
+
if (nodeTooOld()) {
|
|
36
|
+
console.error(
|
|
37
|
+
`myagent requires Bun or Node.js >= 22.19 (found Node ${process.versions.node}).\n` +
|
|
38
|
+
"Install Bun: https://bun.sh",
|
|
39
|
+
);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
launch(process.execPath);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
console.error(`myagent: failed to start ${runtime}: ${err.message}`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
child.once("exit", (code) => process.exit(code ?? 1));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
launch("bun");
|
package/dist/agent/sessions.js
CHANGED
|
@@ -5,9 +5,8 @@
|
|
|
5
5
|
// remembers which thread belongs to which working directory so `-c`/`-r`
|
|
6
6
|
// resume the right conversation. Never contains credentials.
|
|
7
7
|
// ---------------------------------------------------------------------------
|
|
8
|
-
import { mkdir, rename } from "node:fs/promises";
|
|
8
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
9
9
|
import { dirname } from "node:path";
|
|
10
|
-
import { fileExists, readJsonFile, writeFile } from "../runtime.js";
|
|
11
10
|
export class SessionIndex {
|
|
12
11
|
path;
|
|
13
12
|
data;
|
|
@@ -17,9 +16,7 @@ export class SessionIndex {
|
|
|
17
16
|
}
|
|
18
17
|
static async load(path) {
|
|
19
18
|
try {
|
|
20
|
-
|
|
21
|
-
return new SessionIndex(path);
|
|
22
|
-
const parsed = await readJsonFile(path);
|
|
19
|
+
const parsed = JSON.parse(await readFile(path, "utf8"));
|
|
23
20
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
24
21
|
return new SessionIndex(path);
|
|
25
22
|
// Normalize instead of trusting the persisted shape: a truncated write
|
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/config.js
CHANGED
|
@@ -7,8 +7,7 @@
|
|
|
7
7
|
// -------------------------------------------------------------------
|
|
8
8
|
import { homedir } from "node:os";
|
|
9
9
|
import { join } from "node:path";
|
|
10
|
-
import { mkdir, chmod, rename, stat } from "node:fs/promises";
|
|
11
|
-
import { fileExists, readJsonFile, writeFile } from "./runtime.js";
|
|
10
|
+
import { mkdir, chmod, readFile, rename, stat, writeFile } from "node:fs/promises";
|
|
12
11
|
export const EMPTY_CONFIG = {
|
|
13
12
|
baseUrl: "",
|
|
14
13
|
integrationId: "",
|
|
@@ -54,10 +53,7 @@ export async function migrateLegacyConfigDir() {
|
|
|
54
53
|
}
|
|
55
54
|
export async function loadConfig() {
|
|
56
55
|
try {
|
|
57
|
-
const
|
|
58
|
-
if (!(await fileExists(path)))
|
|
59
|
-
return { ...EMPTY_CONFIG };
|
|
60
|
-
const data = await readJsonFile(path);
|
|
56
|
+
const data = JSON.parse(await readFile(configPath(), "utf8"));
|
|
61
57
|
const merged = { ...EMPTY_CONFIG, ...data };
|
|
62
58
|
// Legacy field: autoApprove:false used to mean "ask" — honour it once.
|
|
63
59
|
if (data.approvalDefault === undefined && data.autoApprove === false) {
|
package/dist/git/status.js
CHANGED
|
@@ -118,7 +118,7 @@ export function mergeGitStatus(porcelain, unstaged, staged) {
|
|
|
118
118
|
const MAX_GIT_OUTPUT_CHARS = 512 * 1024;
|
|
119
119
|
async function gitOutput(workspace, args, signal, okExitCodes) {
|
|
120
120
|
try {
|
|
121
|
-
const proc = spawnProcess(["git", "-C", workspace, ...args], { stdout: "pipe", stderr: "ignore"
|
|
121
|
+
const proc = spawnProcess(["git", "-C", workspace, ...args], { stdout: "pipe", stderr: "ignore" });
|
|
122
122
|
const onAbort = () => { try {
|
|
123
123
|
proc.kill();
|
|
124
124
|
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
1
|
// ---------------------------------------------------------------------------
|
|
3
2
|
// myagent CLI — entry point.
|
|
4
3
|
//
|
|
@@ -10,7 +9,7 @@ import { createInterface } from "node:readline/promises";
|
|
|
10
9
|
import { stdin as input, stdout as output } from "node:process";
|
|
11
10
|
import { homedir } from "node:os";
|
|
12
11
|
import { resolve } from "node:path";
|
|
13
|
-
import
|
|
12
|
+
import pkg from "../package.json" with { type: "json" };
|
|
14
13
|
import { IntegrationClient } from "./protocol/client.js";
|
|
15
14
|
import { ToolRegistry } from "./tools/registry.js";
|
|
16
15
|
import { combinedIntegrationUrl, configDir, isConfigured, loadConfig, migrateLegacyConfigDir, resolveConfig, saveConfig, sessionsPath, splitIntegrationUrl, } from "./config.js";
|
|
@@ -20,20 +19,13 @@ import { runTui } from "./ui/app.js";
|
|
|
20
19
|
import { failureDoneEvent, failureResult, runHeadless } from "./headless.js";
|
|
21
20
|
import { discoverSkills, formatSkillList, resolveSkillDirs } from "./skills/discovery.js";
|
|
22
21
|
import { sanitizeForTerminal } from "./sanitize.js";
|
|
23
|
-
import { isMainModule,
|
|
22
|
+
import { isMainModule, readStdinText } from "./runtime.js";
|
|
24
23
|
/**
|
|
25
|
-
* Package version,
|
|
26
|
-
*
|
|
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).
|
|
27
27
|
*/
|
|
28
|
-
|
|
29
|
-
try {
|
|
30
|
-
const pkg = await readJsonFile(fileURLToPath(new URL("../package.json", import.meta.url)));
|
|
31
|
-
return pkg.version ?? "0.0.0";
|
|
32
|
-
}
|
|
33
|
-
catch {
|
|
34
|
-
return "0.0.0";
|
|
35
|
-
}
|
|
36
|
-
}
|
|
28
|
+
const VERSION = pkg.version;
|
|
37
29
|
const HELP = `myagent — a coding agent for your local repository, powered by MyAgent
|
|
38
30
|
|
|
39
31
|
Usage:
|
|
@@ -305,7 +297,7 @@ async function main() {
|
|
|
305
297
|
return 0;
|
|
306
298
|
}
|
|
307
299
|
if (args.command === "version") {
|
|
308
|
-
process.stdout.write(`${
|
|
300
|
+
process.stdout.write(`${VERSION}\n`);
|
|
309
301
|
return 0;
|
|
310
302
|
}
|
|
311
303
|
const stored = await loadConfig();
|
package/dist/runtime.d.ts
CHANGED
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
export declare const isBun: boolean;
|
|
2
|
-
export declare function fileExists(path: string): Promise<boolean>;
|
|
3
|
-
export declare function readTextFile(path: string): Promise<string>;
|
|
4
|
-
export declare function readJsonFile<T>(path: string): Promise<T>;
|
|
5
|
-
export declare function writeFile(path: string, data: string): Promise<void>;
|
|
6
|
-
/** Resolve an executable on PATH (Bun.which on Bun, a PATH scan on Node). */
|
|
7
|
-
export declare function which(cmd: string): string | null;
|
|
8
1
|
/** Read all of stdin as UTF-8 text. Callers check `process.stdin.isTTY` first. */
|
|
9
2
|
export declare function readStdinText(): Promise<string>;
|
|
10
3
|
/**
|
|
@@ -14,25 +7,25 @@ export declare function readStdinText(): Promise<string>;
|
|
|
14
7
|
* comparing the resolved entry path (realpath, so npm/bun bin symlinks match).
|
|
15
8
|
*/
|
|
16
9
|
export declare function isMainModule(meta: ImportMeta): boolean;
|
|
17
|
-
export interface ProcessStdin {
|
|
18
|
-
write(data: Uint8Array): void;
|
|
19
|
-
end(): void;
|
|
20
|
-
}
|
|
21
10
|
export interface ProcessHandle {
|
|
22
11
|
pid: number;
|
|
23
12
|
stdout: ReadableStream<Uint8Array>;
|
|
24
13
|
stderr: ReadableStream<Uint8Array>;
|
|
25
|
-
stdin: ProcessStdin;
|
|
26
14
|
exited: Promise<number>;
|
|
27
15
|
kill(signal?: number | NodeJS.Signals): void;
|
|
28
16
|
}
|
|
29
17
|
export interface SpawnOptions {
|
|
30
18
|
cwd?: string;
|
|
31
|
-
env?: Record<string, string>;
|
|
32
|
-
stdin?: "pipe" | "ignore";
|
|
33
19
|
stdout?: "pipe" | "ignore";
|
|
34
20
|
stderr?: "pipe" | "ignore";
|
|
35
21
|
/** POSIX: run in a new process group so `process.kill(-pid)` reaches the tree. */
|
|
36
22
|
detached?: boolean;
|
|
37
23
|
}
|
|
38
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
|
@@ -1,74 +1,19 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
|
-
// Runtime shims:
|
|
2
|
+
// Runtime shims: the few places where Bun and Node genuinely differ.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// the emitted JS never touches the
|
|
8
|
-
//
|
|
9
|
-
// The subprocess shim normalizes both runtimes to one `ProcessHandle` (Web
|
|
10
|
-
// ReadableStreams for stdout/stderr, a FileSink-like stdin, `.pid`, `.kill()`,
|
|
11
|
-
// `.exited`) — the same shape `Bun.spawn` returns, so the shell/git/grep tools
|
|
12
|
-
// run unchanged on either runtime.
|
|
4
|
+
// Everything else uses node: builtins, which Bun implements natively — only
|
|
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.
|
|
13
9
|
// ---------------------------------------------------------------------------
|
|
14
10
|
import { spawn } from "node:child_process";
|
|
15
|
-
import { realpathSync
|
|
16
|
-
import { readFile, stat, writeFile as writeFileFs } from "node:fs/promises";
|
|
11
|
+
import { realpathSync } from "node:fs";
|
|
17
12
|
import { Buffer } from "node:buffer";
|
|
18
|
-
import {
|
|
13
|
+
import { constants as osConstants } from "node:os";
|
|
19
14
|
import { Readable } from "node:stream";
|
|
20
15
|
import { fileURLToPath } from "node:url";
|
|
21
|
-
|
|
22
|
-
// --- files -----------------------------------------------------------------
|
|
23
|
-
export async function fileExists(path) {
|
|
24
|
-
if (isBun)
|
|
25
|
-
return Bun.file(path).exists();
|
|
26
|
-
try {
|
|
27
|
-
return (await stat(path)).isFile();
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
export async function readTextFile(path) {
|
|
34
|
-
if (isBun)
|
|
35
|
-
return Bun.file(path).text();
|
|
36
|
-
return readFile(path, "utf8");
|
|
37
|
-
}
|
|
38
|
-
export async function readJsonFile(path) {
|
|
39
|
-
return JSON.parse(await readTextFile(path));
|
|
40
|
-
}
|
|
41
|
-
export async function writeFile(path, data) {
|
|
42
|
-
if (isBun) {
|
|
43
|
-
await Bun.write(path, data);
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
await writeFileFs(path, data);
|
|
47
|
-
}
|
|
48
|
-
// --- which -----------------------------------------------------------------
|
|
49
|
-
/** Resolve an executable on PATH (Bun.which on Bun, a PATH scan on Node). */
|
|
50
|
-
export function which(cmd) {
|
|
51
|
-
if (isBun && Bun.which)
|
|
52
|
-
return Bun.which(cmd);
|
|
53
|
-
const dirs = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
54
|
-
const exts = process.platform === "win32"
|
|
55
|
-
? (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean)
|
|
56
|
-
: [""];
|
|
57
|
-
const names = cmd.includes(".") ? [cmd] : exts.map((ext) => cmd + ext);
|
|
58
|
-
for (const dir of dirs) {
|
|
59
|
-
for (const name of names) {
|
|
60
|
-
const candidate = join(dir, name);
|
|
61
|
-
try {
|
|
62
|
-
if (statSync(candidate).isFile())
|
|
63
|
-
return candidate;
|
|
64
|
-
}
|
|
65
|
-
catch {
|
|
66
|
-
/* keep looking */
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
16
|
+
const isBun = typeof Bun !== "undefined";
|
|
72
17
|
// --- stdin -----------------------------------------------------------------
|
|
73
18
|
/** Read all of stdin as UTF-8 text. Callers check `process.stdin.isTTY` first. */
|
|
74
19
|
export async function readStdinText() {
|
|
@@ -103,23 +48,51 @@ export function isMainModule(meta) {
|
|
|
103
48
|
export function spawnProcess(args, opts) {
|
|
104
49
|
return isBun ? spawnBun(args, opts) : spawnNode(args, opts);
|
|
105
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
|
+
}
|
|
106
68
|
function spawnBun(args, opts) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
+
}
|
|
115
92
|
return {
|
|
116
93
|
pid: proc.pid,
|
|
117
|
-
stdout: proc.stdout,
|
|
118
|
-
stderr: proc.stderr,
|
|
119
|
-
stdin: {
|
|
120
|
-
write: (data) => { proc.stdin.write(data); },
|
|
121
|
-
end: () => { proc.stdin.end(); },
|
|
122
|
-
},
|
|
94
|
+
stdout: opts?.stdout === "pipe" ? proc.stdout : emptyStream(),
|
|
95
|
+
stderr: opts?.stderr === "pipe" ? proc.stderr : emptyStream(),
|
|
123
96
|
exited: proc.exited,
|
|
124
97
|
kill: (signal) => { try {
|
|
125
98
|
proc.kill(signal);
|
|
@@ -130,9 +103,8 @@ function spawnBun(args, opts) {
|
|
|
130
103
|
function spawnNode(args, opts) {
|
|
131
104
|
const child = spawn(args[0], args.slice(1), {
|
|
132
105
|
cwd: opts?.cwd,
|
|
133
|
-
env: opts?.env ? { ...process.env, ...opts.env } : process.env,
|
|
134
106
|
stdio: [
|
|
135
|
-
|
|
107
|
+
"ignore",
|
|
136
108
|
opts?.stdout === "pipe" ? "pipe" : "ignore",
|
|
137
109
|
opts?.stderr === "pipe" ? "pipe" : "ignore",
|
|
138
110
|
],
|
|
@@ -140,23 +112,17 @@ function spawnNode(args, opts) {
|
|
|
140
112
|
});
|
|
141
113
|
// First of close/error wins: a spawn failure (ENOENT) may emit only `error`.
|
|
142
114
|
const exited = new Promise((resolve) => {
|
|
143
|
-
child.once("close", (code) => resolve(code
|
|
115
|
+
child.once("close", (code, signal) => resolve(exitCodeFromClose(code, signal)));
|
|
144
116
|
child.once("error", () => resolve(-1));
|
|
145
117
|
});
|
|
146
118
|
return {
|
|
147
119
|
pid: child.pid ?? 0,
|
|
148
120
|
stdout: child.stdout
|
|
149
121
|
? Readable.toWeb(child.stdout)
|
|
150
|
-
:
|
|
122
|
+
: emptyStream(),
|
|
151
123
|
stderr: child.stderr
|
|
152
124
|
? Readable.toWeb(child.stderr)
|
|
153
|
-
:
|
|
154
|
-
stdin: {
|
|
155
|
-
write: (data) => { if (child.stdin)
|
|
156
|
-
child.stdin.write(Buffer.from(data)); },
|
|
157
|
-
end: () => { if (child.stdin)
|
|
158
|
-
child.stdin.end(); },
|
|
159
|
-
},
|
|
125
|
+
: emptyStream(),
|
|
160
126
|
exited,
|
|
161
127
|
kill: (signal) => { try {
|
|
162
128
|
child.kill(signal);
|
package/dist/skills/discovery.js
CHANGED
|
@@ -8,9 +8,8 @@
|
|
|
8
8
|
// 2. <workspace>/.myagent/skills
|
|
9
9
|
// 3. <configDir>/skills (created on first run so there's always a home)
|
|
10
10
|
// -------------------------------------------------------------------
|
|
11
|
-
import { mkdir, readdir, stat } from "node:fs/promises";
|
|
11
|
+
import { mkdir, readFile, readdir, stat } from "node:fs/promises";
|
|
12
12
|
import { join, resolve } from "node:path";
|
|
13
|
-
import { readTextFile } from "../runtime.js";
|
|
14
13
|
/** Parse the leading `--- ... ---` frontmatter for name/description. */
|
|
15
14
|
export function parseFrontmatter(md) {
|
|
16
15
|
const match = md.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
@@ -27,7 +26,7 @@ export function parseFrontmatter(md) {
|
|
|
27
26
|
async function loadSkillDir(dir, fallbackName) {
|
|
28
27
|
let md;
|
|
29
28
|
try {
|
|
30
|
-
md = await
|
|
29
|
+
md = await readFile(join(dir, "SKILL.md"), "utf8");
|
|
31
30
|
}
|
|
32
31
|
catch {
|
|
33
32
|
return null;
|
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
|
}
|
|
@@ -106,10 +105,10 @@ async function searchWithRipgrep(pattern, searchPath, include, signal) {
|
|
|
106
105
|
if (buffer)
|
|
107
106
|
consume(buffer);
|
|
108
107
|
const code = await proc.exited;
|
|
109
|
-
// rg exits
|
|
110
|
-
// failure —
|
|
111
|
-
//
|
|
112
|
-
if (code
|
|
108
|
+
// rg exits 0 (matches) or 1 (no matches); anything else — a rejected
|
|
109
|
+
// pattern, a spawn failure (-1) — falls back to the JS walk rather than
|
|
110
|
+
// reading as "none".
|
|
111
|
+
if (code !== 0 && code !== 1)
|
|
113
112
|
return null;
|
|
114
113
|
return matches;
|
|
115
114
|
}
|
package/dist/tools/shell.js
CHANGED
|
@@ -69,10 +69,8 @@ const shellTool = defineTool({
|
|
|
69
69
|
const detached = process.platform !== "win32";
|
|
70
70
|
const proc = spawnProcess(shell, {
|
|
71
71
|
cwd: ctx.workspace,
|
|
72
|
-
env: process.env,
|
|
73
72
|
stdout: "pipe",
|
|
74
73
|
stderr: "pipe",
|
|
75
|
-
stdin: "ignore",
|
|
76
74
|
// POSIX: a new process group lets us kill the whole pipeline (a bare
|
|
77
75
|
// proc.kill() only signals the shell, and grandchildren keep the pipes
|
|
78
76
|
// open, delaying the timeout unwind).
|
package/dist/ui/app.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// Owns the slash commands, keybindings, approvals, resume/render of remote
|
|
6
6
|
// history and the git status polling that feeds the right panel.
|
|
7
7
|
// ---------------------------------------------------------------------------
|
|
8
|
+
import { writeFile } from "node:fs/promises";
|
|
8
9
|
import { basename, join, resolve } from "node:path";
|
|
9
10
|
import { CombinedAutocompleteProvider, Box, Container, HStack, isViewportTUI, matchesKey, MouseRegion, ProcessTerminal, ScrollView, SelectList, Spacer, TuiAltScreen, VStack, visibleWidth, } from "@earendil-works/pi-tui";
|
|
10
11
|
import { AgentRunner, usageFromSession } from "../agent/turn.js";
|
|
@@ -14,7 +15,6 @@ import { deriveTitle } from "../agent/sessions.js";
|
|
|
14
15
|
import { TodoStore } from "../agent/todo.js";
|
|
15
16
|
import { ApprovalPolicy } from "../approval/policy.js";
|
|
16
17
|
import { collectFileDiff, collectGitStatus, EMPTY_GIT_STATUS } from "../git/status.js";
|
|
17
|
-
import { writeFile } from "../runtime.js";
|
|
18
18
|
import { bgRgb, isColorEnabled, modeBorder, overlayBackgroundRgb, panelBackgroundRgb, style, userMessageBackgroundRgb } from "./colors.js";
|
|
19
19
|
import { helpLines } from "./help.js";
|
|
20
20
|
import { buildModelItems, currentModelId } from "./model-list.js";
|
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;
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ocis/myagent-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Terminal coding agent for your local repository, powered by a remote MyAgent server",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"myagent": "./
|
|
7
|
+
"myagent": "./bin/myagent.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
|
+
"bin",
|
|
10
11
|
"dist/**/*.js",
|
|
11
12
|
"dist/**/*.d.ts",
|
|
12
13
|
"skills"
|