@ocis/myagent-cli 0.2.0 → 0.2.1

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
@@ -36,15 +36,15 @@ local repo (cwd) remote myagent
36
36
  ## Install
37
37
 
38
38
  ```bash
39
- npm install -g @ocis/myagent-cli # global (Node ≥22.19)
40
- bunx @ocis/myagent-cli # one-off, no install (Bun)
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 **Node.js ≥22.19** or **Bun ≥1.0** — it detects the runtime it was
44
- launched with. The published bin uses a Node shebang, so `bun install -g` also runs
45
- it under Node; use `bunx` (or `bun <path-to-bin>`) to run it on Bun. Configure
46
- credentials with `myagent config`, or set `MYAGENT_BASE_URL` /
47
- `MYAGENT_INTEGRATION_ID` / `MYAGENT_API_KEY`.
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,47 @@
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
+ function launch(runtime) {
25
+ child = spawn(runtime, [entry, ...args], { stdio: "inherit" });
26
+
27
+ child.once("error", (err) => {
28
+ if (err.code === "ENOENT" && runtime !== process.execPath) {
29
+ const [major, minor] = process.versions.node.split(".").map(Number);
30
+ if (major < 22 || (major === 22 && minor < 19)) {
31
+ console.error(
32
+ `myagent requires Bun or Node.js >= 22.19 (found Node ${process.versions.node}).\n` +
33
+ "Install Bun: https://bun.sh",
34
+ );
35
+ process.exit(1);
36
+ }
37
+ launch(process.execPath);
38
+ return;
39
+ }
40
+ console.error(`myagent: failed to start ${runtime}: ${err.message}`);
41
+ process.exit(1);
42
+ });
43
+
44
+ child.once("exit", (code) => process.exit(code ?? 1));
45
+ }
46
+
47
+ launch("bun");
@@ -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
- if (!(await fileExists(path)))
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/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 path = configPath();
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) {
@@ -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", stdin: "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
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env node
2
1
  import { type StoredConfig } from "./config.js";
3
2
  import { type AgentMode } from "./agent/modes.js";
4
3
  import { type OutputFormat } from "./headless.js";
package/dist/index.js CHANGED
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env node
2
1
  // ---------------------------------------------------------------------------
3
2
  // myagent CLI — entry point.
4
3
  //
@@ -9,8 +8,8 @@
9
8
  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";
11
+ import { readFile } from "node:fs/promises";
12
12
  import { resolve } from "node:path";
13
- import { fileURLToPath } from "node:url";
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,14 +19,14 @@ 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, readJsonFile, readStdinText } from "./runtime.js";
22
+ import { isMainModule, readStdinText } from "./runtime.js";
24
23
  /**
25
24
  * Package version, read from package.json so npm releases never drift from
26
25
  * `--version`. Resolves to the package root from both src/ (dev) and dist/.
27
26
  */
28
27
  async function readVersion() {
29
28
  try {
30
- const pkg = await readJsonFile(fileURLToPath(new URL("../package.json", import.meta.url)));
29
+ const pkg = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
31
30
  return pkg.version ?? "0.0.0";
32
31
  }
33
32
  catch {
package/dist/runtime.d.ts CHANGED
@@ -1,8 +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
1
  /** Resolve an executable on PATH (Bun.which on Bun, a PATH scan on Node). */
7
2
  export declare function which(cmd: string): string | null;
8
3
  /** Read all of stdin as UTF-8 text. Callers check `process.stdin.isTTY` first. */
@@ -14,22 +9,15 @@ export declare function readStdinText(): Promise<string>;
14
9
  * comparing the resolved entry path (realpath, so npm/bun bin symlinks match).
15
10
  */
16
11
  export declare function isMainModule(meta: ImportMeta): boolean;
17
- export interface ProcessStdin {
18
- write(data: Uint8Array): void;
19
- end(): void;
20
- }
21
12
  export interface ProcessHandle {
22
13
  pid: number;
23
14
  stdout: ReadableStream<Uint8Array>;
24
15
  stderr: ReadableStream<Uint8Array>;
25
- stdin: ProcessStdin;
26
16
  exited: Promise<number>;
27
17
  kill(signal?: number | NodeJS.Signals): void;
28
18
  }
29
19
  export interface SpawnOptions {
30
20
  cwd?: string;
31
- env?: Record<string, string>;
32
- stdin?: "pipe" | "ignore";
33
21
  stdout?: "pipe" | "ignore";
34
22
  stderr?: "pipe" | "ignore";
35
23
  /** POSIX: run in a new process group so `process.kill(-pid)` reaches the tree. */
package/dist/runtime.js CHANGED
@@ -1,63 +1,30 @@
1
1
  // ---------------------------------------------------------------------------
2
- // Runtime shims: Bun APIs on Bun, node: equivalents on Node.
2
+ // Runtime shims: the few places where Bun and Node genuinely differ.
3
3
  //
4
- // The CLI runs on both runtimes. Bun's file/spawn/stdin/which globals have no
5
- // Node counterpart, so every call site goes through this module instead of
6
- // touching `Bun.*` directly. The Bun branches are guarded by `typeof Bun`, so
7
- // the emitted JS never touches the `Bun` global on Node.
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), PATH lookup
6
+ // (Bun.which), stdin (Bun.stdin) and entry-point detection (import.meta.main)
7
+ // need a runtime branch. The Bun branches are guarded by `typeof Bun`, so the
8
+ // emitted JS never touches the `Bun` global on Node.
13
9
  // ---------------------------------------------------------------------------
14
10
  import { spawn } from "node:child_process";
15
11
  import { realpathSync, statSync } from "node:fs";
16
- import { readFile, stat, writeFile as writeFileFs } from "node:fs/promises";
17
12
  import { Buffer } from "node:buffer";
18
13
  import { delimiter, join } from "node:path";
19
14
  import { Readable } from "node:stream";
20
15
  import { fileURLToPath } from "node:url";
21
- export const isBun = typeof Bun !== "undefined";
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
- }
16
+ const isBun = typeof Bun !== "undefined";
48
17
  // --- which -----------------------------------------------------------------
49
18
  /** Resolve an executable on PATH (Bun.which on Bun, a PATH scan on Node). */
50
19
  export function which(cmd) {
51
20
  if (isBun && Bun.which)
52
21
  return Bun.which(cmd);
53
- const dirs = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
54
22
  const exts = process.platform === "win32"
55
23
  ? (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean)
56
24
  : [""];
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);
25
+ for (const dir of (process.env.PATH ?? "").split(delimiter)) {
26
+ for (const ext of exts) {
27
+ const candidate = join(dir, cmd + ext);
61
28
  try {
62
29
  if (statSync(candidate).isFile())
63
30
  return candidate;
@@ -106,8 +73,7 @@ export function spawnProcess(args, opts) {
106
73
  function spawnBun(args, opts) {
107
74
  const proc = Bun.spawn(args, {
108
75
  cwd: opts?.cwd,
109
- env: opts?.env ? { ...process.env, ...opts.env } : undefined,
110
- stdin: opts?.stdin === "pipe" ? "pipe" : "ignore",
76
+ stdin: "ignore",
111
77
  stdout: opts?.stdout === "pipe" ? "pipe" : "ignore",
112
78
  stderr: opts?.stderr === "pipe" ? "pipe" : "ignore",
113
79
  detached: opts?.detached,
@@ -116,10 +82,6 @@ function spawnBun(args, opts) {
116
82
  pid: proc.pid,
117
83
  stdout: proc.stdout,
118
84
  stderr: proc.stderr,
119
- stdin: {
120
- write: (data) => { proc.stdin.write(data); },
121
- end: () => { proc.stdin.end(); },
122
- },
123
85
  exited: proc.exited,
124
86
  kill: (signal) => { try {
125
87
  proc.kill(signal);
@@ -130,9 +92,8 @@ function spawnBun(args, opts) {
130
92
  function spawnNode(args, opts) {
131
93
  const child = spawn(args[0], args.slice(1), {
132
94
  cwd: opts?.cwd,
133
- env: opts?.env ? { ...process.env, ...opts.env } : process.env,
134
95
  stdio: [
135
- opts?.stdin === "pipe" ? "pipe" : "ignore",
96
+ "ignore",
136
97
  opts?.stdout === "pipe" ? "pipe" : "ignore",
137
98
  opts?.stderr === "pipe" ? "pipe" : "ignore",
138
99
  ],
@@ -151,12 +112,6 @@ function spawnNode(args, opts) {
151
112
  stderr: child.stderr
152
113
  ? Readable.toWeb(child.stderr)
153
114
  : new ReadableStream(),
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
- },
160
115
  exited,
161
116
  kill: (signal) => { try {
162
117
  child.kill(signal);
@@ -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 readTextFile(join(dir, "SKILL.md"));
29
+ md = await readFile(join(dir, "SKILL.md"), "utf8");
31
30
  }
32
31
  catch {
33
32
  return null;
@@ -106,10 +106,10 @@ async function searchWithRipgrep(pattern, searchPath, include, signal) {
106
106
  if (buffer)
107
107
  consume(buffer);
108
108
  const code = await proc.exited;
109
- // rg exits 1 for "no matches" (an answer); any other non-zero exit is a
110
- // failure — e.g. a JS-valid pattern (backreference, lookahead) its engine
111
- // rejects — and must fall back to the JS walk rather than read as "none".
112
- if (code > 1)
109
+ // rg exits 0 (matches) or 1 (no matches); anything else — a rejected
110
+ // pattern, a spawn failure (-1) — falls back to the JS walk rather than
111
+ // reading as "none".
112
+ if (code !== 0 && code !== 1)
113
113
  return null;
114
114
  return matches;
115
115
  }
@@ -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/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@ocis/myagent-cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
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": "./dist/index.js"
7
+ "myagent": "./bin/myagent.js"
8
8
  },
9
9
  "files": [
10
+ "bin",
10
11
  "dist/**/*.js",
11
12
  "dist/**/*.d.ts",
12
13
  "skills"