@ocis/myagent-cli 0.2.0
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 +357 -0
- package/dist/agent/context.d.ts +33 -0
- package/dist/agent/context.js +169 -0
- package/dist/agent/modes.d.ts +21 -0
- package/dist/agent/modes.js +84 -0
- package/dist/agent/prompt-builder.d.ts +9 -0
- package/dist/agent/prompt-builder.js +31 -0
- package/dist/agent/sessions.d.ts +38 -0
- package/dist/agent/sessions.js +130 -0
- package/dist/agent/todo.d.ts +18 -0
- package/dist/agent/todo.js +61 -0
- package/dist/agent/turn.d.ts +309 -0
- package/dist/agent/turn.js +1253 -0
- package/dist/approval/policy.d.ts +81 -0
- package/dist/approval/policy.js +157 -0
- package/dist/config.d.ts +49 -0
- package/dist/config.js +156 -0
- package/dist/git/status.d.ts +89 -0
- package/dist/git/status.js +226 -0
- package/dist/headless.d.ts +72 -0
- package/dist/headless.js +330 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.js +511 -0
- package/dist/protocol/client.d.ts +123 -0
- package/dist/protocol/client.js +250 -0
- package/dist/protocol/sse-frames.d.ts +6 -0
- package/dist/protocol/sse-frames.js +75 -0
- package/dist/protocol/types.d.ts +200 -0
- package/dist/protocol/types.js +8 -0
- package/dist/runtime.d.ts +38 -0
- package/dist/runtime.js +166 -0
- package/dist/sanitize.d.ts +1 -0
- package/dist/sanitize.js +21 -0
- package/dist/skills/discovery.d.ts +24 -0
- package/dist/skills/discovery.js +109 -0
- package/dist/tools/binary.d.ts +2 -0
- package/dist/tools/binary.js +22 -0
- package/dist/tools/diff.d.ts +1 -0
- package/dist/tools/diff.js +49 -0
- package/dist/tools/find.d.ts +2 -0
- package/dist/tools/find.js +61 -0
- package/dist/tools/fs.d.ts +2 -0
- package/dist/tools/fs.js +276 -0
- package/dist/tools/glob.d.ts +6 -0
- package/dist/tools/glob.js +131 -0
- package/dist/tools/grep.d.ts +3 -0
- package/dist/tools/grep.js +228 -0
- package/dist/tools/paths.d.ts +27 -0
- package/dist/tools/paths.js +124 -0
- package/dist/tools/registry.d.ts +13 -0
- package/dist/tools/registry.js +38 -0
- package/dist/tools/shell.d.ts +2 -0
- package/dist/tools/shell.js +136 -0
- package/dist/tools/skills.d.ts +2 -0
- package/dist/tools/skills.js +36 -0
- package/dist/tools/todo.d.ts +2 -0
- package/dist/tools/todo.js +43 -0
- package/dist/tools/transfer.d.ts +2 -0
- package/dist/tools/transfer.js +145 -0
- package/dist/tools/truncate.d.ts +12 -0
- package/dist/tools/truncate.js +46 -0
- package/dist/tools/types.d.ts +85 -0
- package/dist/tools/types.js +63 -0
- package/dist/ui/app.d.ts +39 -0
- package/dist/ui/app.js +1061 -0
- package/dist/ui/colors.d.ts +100 -0
- package/dist/ui/colors.js +169 -0
- package/dist/ui/components.d.ts +267 -0
- package/dist/ui/components.js +811 -0
- package/dist/ui/diff.d.ts +37 -0
- package/dist/ui/diff.js +143 -0
- package/dist/ui/format.d.ts +28 -0
- package/dist/ui/format.js +76 -0
- package/dist/ui/help.d.ts +6 -0
- package/dist/ui/help.js +45 -0
- package/dist/ui/highlight.d.ts +20 -0
- package/dist/ui/highlight.js +210 -0
- package/dist/ui/logo.d.ts +24 -0
- package/dist/ui/logo.js +106 -0
- package/dist/ui/model-list.d.ts +10 -0
- package/dist/ui/model-list.js +33 -0
- package/dist/ui/quit-confirm.d.ts +8 -0
- package/dist/ui/quit-confirm.js +40 -0
- package/dist/ui/select-popup.d.ts +30 -0
- package/dist/ui/select-popup.js +54 -0
- package/dist/ui/theme.d.ts +4 -0
- package/dist/ui/theme.js +41 -0
- package/dist/ui/tool-view.d.ts +20 -0
- package/dist/ui/tool-view.js +326 -0
- package/package.json +44 -0
- package/skills/git-commit/SKILL.md +27 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// myagent CLI — entry point.
|
|
4
|
+
//
|
|
5
|
+
// Interactive TUI by default; `-p`/`run`/piped stdin switch to headless mode.
|
|
6
|
+
// Credentials resolve flags > env > config file; the config wizard lives under
|
|
7
|
+
// `myagent config`.
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
import { createInterface } from "node:readline/promises";
|
|
10
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { resolve } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { IntegrationClient } from "./protocol/client.js";
|
|
15
|
+
import { ToolRegistry } from "./tools/registry.js";
|
|
16
|
+
import { combinedIntegrationUrl, configDir, isConfigured, loadConfig, migrateLegacyConfigDir, resolveConfig, saveConfig, sessionsPath, splitIntegrationUrl, } from "./config.js";
|
|
17
|
+
import { SessionIndex } from "./agent/sessions.js";
|
|
18
|
+
import { DEFAULT_MODE } from "./agent/modes.js";
|
|
19
|
+
import { runTui } from "./ui/app.js";
|
|
20
|
+
import { failureDoneEvent, failureResult, runHeadless } from "./headless.js";
|
|
21
|
+
import { discoverSkills, formatSkillList, resolveSkillDirs } from "./skills/discovery.js";
|
|
22
|
+
import { sanitizeForTerminal } from "./sanitize.js";
|
|
23
|
+
import { isMainModule, readJsonFile, readStdinText } from "./runtime.js";
|
|
24
|
+
/**
|
|
25
|
+
* Package version, read from package.json so npm releases never drift from
|
|
26
|
+
* `--version`. Resolves to the package root from both src/ (dev) and dist/.
|
|
27
|
+
*/
|
|
28
|
+
async function readVersion() {
|
|
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
|
+
}
|
|
37
|
+
const HELP = `myagent — a coding agent for your local repository, powered by MyAgent
|
|
38
|
+
|
|
39
|
+
Usage:
|
|
40
|
+
myagent [options] [prompt...] interactive TUI (or headless if not a TTY)
|
|
41
|
+
myagent run [options] [prompt...] one-shot; prompt from args or stdin
|
|
42
|
+
myagent -p "prompt" same as run
|
|
43
|
+
myagent --prompt "prompt" prompt mode: unattended one-shot (CI)
|
|
44
|
+
myagent config configure credentials interactively
|
|
45
|
+
myagent config show print the current config (redacted)
|
|
46
|
+
myagent skills list locally installed skills
|
|
47
|
+
myagent sessions list remote sessions
|
|
48
|
+
|
|
49
|
+
Connection:
|
|
50
|
+
--base-url <url> MyAgent base URL (env MYAGENT_BASE_URL)
|
|
51
|
+
--integration-id <id> integration id (env MYAGENT_INTEGRATION_ID)
|
|
52
|
+
--api-key <key> integration API key (env MYAGENT_API_KEY)
|
|
53
|
+
|
|
54
|
+
Session:
|
|
55
|
+
-c, --continue continue the most recent session for this directory
|
|
56
|
+
-r, --resume resume the most recent session (same as -c)
|
|
57
|
+
--session <thread-id> resume a specific remote session
|
|
58
|
+
|
|
59
|
+
Agent:
|
|
60
|
+
--plan start in plan mode (read-only)
|
|
61
|
+
--coding start in coding mode (software work; default)
|
|
62
|
+
--cowork start in cowork mode (general tasks)
|
|
63
|
+
--ask confirm mutating tools (interactive only; headless denies them)
|
|
64
|
+
--auto run tools unattended (default)
|
|
65
|
+
--model <id> model for new sessions (client-choice integrations; see /v1/models)
|
|
66
|
+
--thinking <level> off | minimal | low | medium | high | xhigh | max | default
|
|
67
|
+
--skills-dir <dir> extra skills directory (repeatable)
|
|
68
|
+
--allow-outside allow tools to touch paths outside the workspace
|
|
69
|
+
--no-context-files do not load AGENTS.md / CLAUDE.md
|
|
70
|
+
|
|
71
|
+
Prompt mode (CI):
|
|
72
|
+
--prompt <text|-> non-interactive one-shot: auto-approves every tool
|
|
73
|
+
(including paths outside the workspace) and prints
|
|
74
|
+
only the final assistant message. "-" reads stdin.
|
|
75
|
+
--timeout <sec> abort the run after N seconds (headless only)
|
|
76
|
+
|
|
77
|
+
Output:
|
|
78
|
+
--output-format <fmt> text | json | stream-json (headless only)
|
|
79
|
+
--no-logo skip the startup logo animation
|
|
80
|
+
-q, --quiet suppress tool/progress output (headless)
|
|
81
|
+
|
|
82
|
+
Exit codes:
|
|
83
|
+
0 success · 1 run failed (incl. timeout) · 2 usage/config/connection error
|
|
84
|
+
130/143 interrupted (SIGINT/SIGTERM)
|
|
85
|
+
|
|
86
|
+
Other:
|
|
87
|
+
-h, --help show this help
|
|
88
|
+
-v, --version show the version
|
|
89
|
+
`;
|
|
90
|
+
export function parseArgs(argv) {
|
|
91
|
+
const args = { command: "chat", flags: {}, skillsDirs: [], promptParts: [] };
|
|
92
|
+
const rest = [];
|
|
93
|
+
for (let i = 0; i < argv.length; i++) {
|
|
94
|
+
const arg = argv[i];
|
|
95
|
+
const next = (name) => {
|
|
96
|
+
const value = argv[++i];
|
|
97
|
+
if (value === undefined)
|
|
98
|
+
throw new Error(`Missing value for ${name}`);
|
|
99
|
+
return value;
|
|
100
|
+
};
|
|
101
|
+
switch (arg) {
|
|
102
|
+
case "run":
|
|
103
|
+
args.command = "run";
|
|
104
|
+
break;
|
|
105
|
+
case "config":
|
|
106
|
+
args.command = "config";
|
|
107
|
+
args.configSub = argv[i + 1] && !argv[i + 1].startsWith("-") ? argv[++i] : undefined;
|
|
108
|
+
break;
|
|
109
|
+
case "skills":
|
|
110
|
+
args.command = "skills";
|
|
111
|
+
break;
|
|
112
|
+
case "sessions":
|
|
113
|
+
args.command = "sessions";
|
|
114
|
+
break;
|
|
115
|
+
case "-h":
|
|
116
|
+
case "--help":
|
|
117
|
+
args.command = "help";
|
|
118
|
+
break;
|
|
119
|
+
case "-v":
|
|
120
|
+
case "--version":
|
|
121
|
+
args.command = "version";
|
|
122
|
+
break;
|
|
123
|
+
case "--base-url":
|
|
124
|
+
args.flags.baseUrl = next(arg);
|
|
125
|
+
break;
|
|
126
|
+
case "--integration-id":
|
|
127
|
+
args.flags.integrationId = next(arg);
|
|
128
|
+
break;
|
|
129
|
+
case "--api-key":
|
|
130
|
+
args.flags.apiKey = next(arg);
|
|
131
|
+
break;
|
|
132
|
+
case "--workspace":
|
|
133
|
+
case "-w":
|
|
134
|
+
args.flags.workspace = next(arg);
|
|
135
|
+
break;
|
|
136
|
+
case "-c":
|
|
137
|
+
case "--continue":
|
|
138
|
+
case "-r":
|
|
139
|
+
case "--resume":
|
|
140
|
+
args.flags.continue = true;
|
|
141
|
+
break;
|
|
142
|
+
case "--session":
|
|
143
|
+
args.flags.session = next(arg);
|
|
144
|
+
break;
|
|
145
|
+
case "--plan":
|
|
146
|
+
args.flags.mode = "plan";
|
|
147
|
+
break;
|
|
148
|
+
case "--coding":
|
|
149
|
+
args.flags.mode = "coding";
|
|
150
|
+
break;
|
|
151
|
+
case "--cowork":
|
|
152
|
+
args.flags.mode = "cowork";
|
|
153
|
+
break;
|
|
154
|
+
case "--ask":
|
|
155
|
+
args.flags.ask = true;
|
|
156
|
+
break;
|
|
157
|
+
case "--auto":
|
|
158
|
+
args.flags.auto = true;
|
|
159
|
+
break;
|
|
160
|
+
case "--thinking":
|
|
161
|
+
args.flags.thinking = next(arg);
|
|
162
|
+
break;
|
|
163
|
+
case "--model":
|
|
164
|
+
args.flags.model = next(arg);
|
|
165
|
+
break;
|
|
166
|
+
case "--skills-dir":
|
|
167
|
+
args.skillsDirs.push(next(arg));
|
|
168
|
+
break;
|
|
169
|
+
case "--allow-outside":
|
|
170
|
+
args.flags.allowOutside = true;
|
|
171
|
+
break;
|
|
172
|
+
case "--no-context-files":
|
|
173
|
+
args.flags.noContextFiles = true;
|
|
174
|
+
break;
|
|
175
|
+
case "--no-logo":
|
|
176
|
+
args.flags.noLogo = true;
|
|
177
|
+
break;
|
|
178
|
+
case "-q":
|
|
179
|
+
case "--quiet":
|
|
180
|
+
args.flags.quiet = true;
|
|
181
|
+
break;
|
|
182
|
+
case "--prompt":
|
|
183
|
+
args.flags.prompt = next(arg);
|
|
184
|
+
break;
|
|
185
|
+
case "--timeout": {
|
|
186
|
+
const raw = next(arg);
|
|
187
|
+
const value = Number(raw);
|
|
188
|
+
// setTimeout coerces delays above 2^31-1 ms to 1 ms — an oversized
|
|
189
|
+
// value would fire immediately, the opposite of the user's intent.
|
|
190
|
+
if (!Number.isFinite(value) || value <= 0 || value * 1000 > 2_147_483_647) {
|
|
191
|
+
throw new Error(`Invalid --timeout "${raw}" (positive seconds, max 2147483)`);
|
|
192
|
+
}
|
|
193
|
+
args.flags.timeout = value;
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
case "--output-format": {
|
|
197
|
+
const value = next(arg);
|
|
198
|
+
if (value !== "text" && value !== "json" && value !== "stream-json") {
|
|
199
|
+
throw new Error(`Invalid --output-format "${value}" (text | json | stream-json)`);
|
|
200
|
+
}
|
|
201
|
+
args.flags.outputFormat = value;
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
case "-p":
|
|
205
|
+
case "--print":
|
|
206
|
+
args.command = args.command === "chat" ? "run" : args.command;
|
|
207
|
+
break;
|
|
208
|
+
default:
|
|
209
|
+
if (arg.startsWith("-") && arg !== "-")
|
|
210
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
211
|
+
rest.push(arg);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
args.promptParts = rest;
|
|
215
|
+
return args;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Interactive means the TUI: a plain `chat` invocation with both streams on a
|
|
219
|
+
* TTY. `run`/`-p`, `--prompt`, `--output-format`, and piped stdin all mean
|
|
220
|
+
* headless.
|
|
221
|
+
*/
|
|
222
|
+
export function isInteractiveMode(args, stdinIsTty, stdoutIsTty) {
|
|
223
|
+
return (args.command === "chat" &&
|
|
224
|
+
!args.flags.outputFormat &&
|
|
225
|
+
!args.flags.prompt &&
|
|
226
|
+
stdinIsTty &&
|
|
227
|
+
stdoutIsTty);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Whether stdin should be read as prompt text. Piped stdin is a prompt for
|
|
231
|
+
* every headless path (`echo fix | myagent`); an inline `--prompt` never reads
|
|
232
|
+
* it (a CI stdin may be an open pipe — reading it would hang), while
|
|
233
|
+
* `--prompt -` reads it explicitly.
|
|
234
|
+
*/
|
|
235
|
+
export function shouldReadStdin(args, interactive) {
|
|
236
|
+
if (interactive)
|
|
237
|
+
return false;
|
|
238
|
+
// An inline --prompt is the whole prompt; `-` and no flag read stdin.
|
|
239
|
+
return args.flags.prompt === undefined || args.flags.prompt === "-";
|
|
240
|
+
}
|
|
241
|
+
/** Join the prompt sources: positional words, `--prompt` (or stdin), piped stdin. */
|
|
242
|
+
export function assemblePrompt(args, stdinText) {
|
|
243
|
+
const promptFromStdin = args.flags.prompt === "-";
|
|
244
|
+
return [
|
|
245
|
+
...args.promptParts,
|
|
246
|
+
promptFromStdin ? "" : args.flags.prompt,
|
|
247
|
+
stdinText.trim(),
|
|
248
|
+
].filter(Boolean).join("\n");
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Report a pre-run failure (usage/config/connection) in the requested output
|
|
252
|
+
* format and return the exit code. `json` always emits a parseable object — a
|
|
253
|
+
* CI parser must never see an empty stdout; `stream-json` emits a terminal
|
|
254
|
+
* `done` event; text goes to stderr.
|
|
255
|
+
*/
|
|
256
|
+
export function fail(format, message, errorCode) {
|
|
257
|
+
if (format === "json") {
|
|
258
|
+
process.stdout.write(failureResult(message, errorCode) + "\n");
|
|
259
|
+
}
|
|
260
|
+
else if (format === "stream-json") {
|
|
261
|
+
process.stdout.write(failureDoneEvent(message, errorCode) + "\n");
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
process.stderr.write(message + "\n");
|
|
265
|
+
}
|
|
266
|
+
return 2;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Read piped stdin as prompt text. Bounded by the run's `--timeout` when one
|
|
270
|
+
* is set: a non-TTY stdin that never closes (a CI misconfiguration like
|
|
271
|
+
* `docker run -i` without `< /dev/null`) would otherwise hang before the
|
|
272
|
+
* run's own timeout control is even created. Returns null on timeout so the
|
|
273
|
+
* caller can fail with a clear message instead of running an empty prompt.
|
|
274
|
+
*/
|
|
275
|
+
export async function readStdin(timeoutMs) {
|
|
276
|
+
if (process.stdin.isTTY)
|
|
277
|
+
return "";
|
|
278
|
+
const read = readStdinText().catch(() => "");
|
|
279
|
+
if (!timeoutMs)
|
|
280
|
+
return read;
|
|
281
|
+
let timer = null;
|
|
282
|
+
try {
|
|
283
|
+
return await Promise.race([
|
|
284
|
+
read,
|
|
285
|
+
new Promise((resolve) => { timer = setTimeout(() => resolve(null), timeoutMs); }),
|
|
286
|
+
]);
|
|
287
|
+
}
|
|
288
|
+
finally {
|
|
289
|
+
if (timer)
|
|
290
|
+
clearTimeout(timer);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
async function main() {
|
|
294
|
+
await migrateLegacyConfigDir();
|
|
295
|
+
let args;
|
|
296
|
+
try {
|
|
297
|
+
args = parseArgs(process.argv.slice(2));
|
|
298
|
+
}
|
|
299
|
+
catch (err) {
|
|
300
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n\n${HELP}`);
|
|
301
|
+
return 2;
|
|
302
|
+
}
|
|
303
|
+
if (args.command === "help") {
|
|
304
|
+
process.stdout.write(HELP);
|
|
305
|
+
return 0;
|
|
306
|
+
}
|
|
307
|
+
if (args.command === "version") {
|
|
308
|
+
process.stdout.write(`${await readVersion()}\n`);
|
|
309
|
+
return 0;
|
|
310
|
+
}
|
|
311
|
+
const stored = await loadConfig();
|
|
312
|
+
const config = resolveConfig(stored, args.flags);
|
|
313
|
+
if (args.command === "config") {
|
|
314
|
+
if (args.configSub === "show") {
|
|
315
|
+
process.stdout.write(`apiBaseUrl: ${combinedIntegrationUrl(config) || "(unset)"}\n` +
|
|
316
|
+
`apiKey: ${config.apiKey ? `${"•".repeat(8)}…${config.apiKey.slice(-4)}` : "(unset)"}\n` +
|
|
317
|
+
`approvalDefault: ${config.approvalDefault}\n` +
|
|
318
|
+
`config file: ${configDir()}/config.json\n`);
|
|
319
|
+
return 0;
|
|
320
|
+
}
|
|
321
|
+
// The wizard edits the FILE, so it must see the file's values — not the
|
|
322
|
+
// env/flag-merged runtime config. Otherwise pressing Enter through the
|
|
323
|
+
// prompts would persist a temporary MYAGENT_API_KEY / --base-url into
|
|
324
|
+
// config.json without ever saying so.
|
|
325
|
+
return await configWizard(stored);
|
|
326
|
+
}
|
|
327
|
+
const workspace = resolveWorkspace(args.flags.workspace);
|
|
328
|
+
const skillDirs = await resolveSkillDirs(workspace, configDir(), args.skillsDirs);
|
|
329
|
+
if (args.command === "skills") {
|
|
330
|
+
const skills = await discoverSkills(skillDirs);
|
|
331
|
+
process.stdout.write(`Search paths:\n${skillDirs.map((d) => ` ${d}`).join("\n")}\n\n${formatSkillList(skills)}\n`);
|
|
332
|
+
return 0;
|
|
333
|
+
}
|
|
334
|
+
if (!isConfigured(config)) {
|
|
335
|
+
return fail(args.flags.outputFormat, "Not configured. Run `myagent config` first, or pass --base-url/--integration-id/--api-key.\n" +
|
|
336
|
+
"Create an integration and API key on the MyAgent Integrations page.", "not_configured");
|
|
337
|
+
}
|
|
338
|
+
const client = new IntegrationClient({
|
|
339
|
+
baseUrl: config.baseUrl,
|
|
340
|
+
integrationId: config.integrationId,
|
|
341
|
+
apiKey: config.apiKey,
|
|
342
|
+
});
|
|
343
|
+
let status;
|
|
344
|
+
try {
|
|
345
|
+
// Bounded connect: a hung server must fail (exit 2) instead of hanging CI.
|
|
346
|
+
status = await client.getStatus({ signal: AbortSignal.timeout(30_000) });
|
|
347
|
+
}
|
|
348
|
+
catch (err) {
|
|
349
|
+
return fail(args.flags.outputFormat, `Could not reach ${config.baseUrl}: ${err instanceof Error ? err.message : String(err)}`, "connection_failed");
|
|
350
|
+
}
|
|
351
|
+
if (!status.enabled) {
|
|
352
|
+
return fail(args.flags.outputFormat, `Integration "${status.label}" is disabled on the server.`, "integration_disabled");
|
|
353
|
+
}
|
|
354
|
+
const sessionIndex = await SessionIndex.load(sessionsPath());
|
|
355
|
+
if (args.command === "sessions") {
|
|
356
|
+
const sessions = await client.listSessions();
|
|
357
|
+
return printSessions(sessions);
|
|
358
|
+
}
|
|
359
|
+
const mode = args.flags.mode ?? DEFAULT_MODE;
|
|
360
|
+
const promptMode = args.flags.prompt !== undefined;
|
|
361
|
+
// Prompt mode is unattended by contract: every tool runs, including paths
|
|
362
|
+
// outside the workspace — no one is present to answer an approval prompt.
|
|
363
|
+
const approval = promptMode
|
|
364
|
+
? "auto"
|
|
365
|
+
: args.flags.ask
|
|
366
|
+
? "ask"
|
|
367
|
+
: args.flags.auto
|
|
368
|
+
? "auto"
|
|
369
|
+
: config.approvalDefault;
|
|
370
|
+
const allowOutside = promptMode ? true : (args.flags.allowOutside ?? false);
|
|
371
|
+
const resumeThreadId = args.flags.session
|
|
372
|
+
?? (args.flags.continue ? sessionIndex.lastThread(workspace) : undefined);
|
|
373
|
+
// `--prompt` is unattended by contract, so an explicit `--ask` cannot be
|
|
374
|
+
// honored — say so instead of silently dropping the safer setting.
|
|
375
|
+
if (promptMode && args.flags.ask) {
|
|
376
|
+
process.stderr.write("Warning: --ask is ignored with --prompt (unattended mode auto-approves every tool, including paths outside the workspace).\n");
|
|
377
|
+
}
|
|
378
|
+
const interactive = isInteractiveMode(args, Boolean(process.stdin.isTTY), Boolean(process.stdout.isTTY));
|
|
379
|
+
const readsStdin = shouldReadStdin(args, interactive);
|
|
380
|
+
// A prompt is already complete, yet stdin is still read (it may carry extra
|
|
381
|
+
// context: `cat log | myagent run "summarize:"`). Reading blocks until EOF,
|
|
382
|
+
// so a non-TTY stdin that never closes (CI `docker run -i` without
|
|
383
|
+
// `< /dev/null`) would hang silently — announce the wait instead.
|
|
384
|
+
if (readsStdin && !process.stdin.isTTY && args.promptParts.length > 0 && !args.flags.timeout) {
|
|
385
|
+
process.stderr.write("Reading additional prompt text from stdin until EOF — pass `< /dev/null` or --timeout <sec> if nothing is piped.\n");
|
|
386
|
+
}
|
|
387
|
+
const stdinText = readsStdin
|
|
388
|
+
? await readStdin(args.flags.timeout ? args.flags.timeout * 1000 : undefined)
|
|
389
|
+
: "";
|
|
390
|
+
if (stdinText === null) {
|
|
391
|
+
return fail(args.flags.outputFormat, `Timed out after ${args.flags.timeout}s waiting for stdin.`, "timeout");
|
|
392
|
+
}
|
|
393
|
+
const prompt = assemblePrompt(args, stdinText);
|
|
394
|
+
const registry = new ToolRegistry();
|
|
395
|
+
if (interactive) {
|
|
396
|
+
return await runTui({
|
|
397
|
+
client,
|
|
398
|
+
registry,
|
|
399
|
+
config,
|
|
400
|
+
sessionIndex,
|
|
401
|
+
workspace,
|
|
402
|
+
allowOutside: args.flags.allowOutside ?? false,
|
|
403
|
+
skillDirs,
|
|
404
|
+
noContextFiles: args.flags.noContextFiles,
|
|
405
|
+
initialMode: mode,
|
|
406
|
+
initialApproval: approval,
|
|
407
|
+
initialThinking: args.flags.thinking === "default" ? undefined : args.flags.thinking,
|
|
408
|
+
initialModel: args.flags.model,
|
|
409
|
+
initialPrompt: prompt || undefined,
|
|
410
|
+
resumeThreadId,
|
|
411
|
+
noLogo: args.flags.noLogo,
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
if (!prompt) {
|
|
415
|
+
return fail(args.flags.outputFormat, "No prompt provided. Usage: myagent -p \"...\" or pipe text via stdin.", "no_prompt");
|
|
416
|
+
}
|
|
417
|
+
return await runHeadless({
|
|
418
|
+
client,
|
|
419
|
+
registry,
|
|
420
|
+
sessionIndex,
|
|
421
|
+
workspace,
|
|
422
|
+
allowOutside,
|
|
423
|
+
skillDirs,
|
|
424
|
+
mode,
|
|
425
|
+
approval,
|
|
426
|
+
model: args.flags.model,
|
|
427
|
+
thinkingLevel: args.flags.thinking === "default" ? undefined : args.flags.thinking,
|
|
428
|
+
noContextFiles: args.flags.noContextFiles,
|
|
429
|
+
resumeThreadId,
|
|
430
|
+
prompt,
|
|
431
|
+
outputFormat: args.flags.outputFormat ?? "text",
|
|
432
|
+
quiet: args.flags.quiet,
|
|
433
|
+
lastMessageOnly: promptMode,
|
|
434
|
+
timeoutMs: args.flags.timeout ? args.flags.timeout * 1000 : undefined,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
function printSessions(sessions) {
|
|
438
|
+
if (sessions.length === 0) {
|
|
439
|
+
process.stdout.write("No remote sessions.\n");
|
|
440
|
+
return 0;
|
|
441
|
+
}
|
|
442
|
+
for (const session of sessions) {
|
|
443
|
+
const updated = new Date(session.updated_at).toLocaleString();
|
|
444
|
+
// Remote free text (title, status) reaches the real terminal here — same
|
|
445
|
+
// sanitizing rule as the TUI transcript and the headless writer.
|
|
446
|
+
const line = `${session.thread_id} ${session.status.padEnd(10)} ${String(session.message_count).padStart(4)} msgs ${updated} ${session.title}`;
|
|
447
|
+
process.stdout.write(`${sanitizeForTerminal(line)}\n`);
|
|
448
|
+
}
|
|
449
|
+
return 0;
|
|
450
|
+
}
|
|
451
|
+
/** Resolve the workspace flag (with ~ expansion) against the process cwd. */
|
|
452
|
+
function resolveWorkspace(flag) {
|
|
453
|
+
if (!flag)
|
|
454
|
+
return process.cwd();
|
|
455
|
+
const expanded = flag.replace(/^~(?=$|\/)/, homedir());
|
|
456
|
+
return resolve(expanded);
|
|
457
|
+
}
|
|
458
|
+
async function configWizard(config) {
|
|
459
|
+
const rl = createInterface({ input, output });
|
|
460
|
+
try {
|
|
461
|
+
// The Integrations page shows one combined URL (`<host>/integrations/<id>`)
|
|
462
|
+
// — accept it here and derive the id, so pasting it just works.
|
|
463
|
+
const baseUrlDefault = combinedIntegrationUrl(config) || "https://...";
|
|
464
|
+
const baseUrlInput = (await rl.question(`API base URL [${baseUrlDefault}]: `)).trim() || config.baseUrl;
|
|
465
|
+
const { baseUrl, integrationId: derivedId } = splitIntegrationUrl(baseUrlInput);
|
|
466
|
+
const integrationIdDefault = derivedId ?? config.integrationId;
|
|
467
|
+
const integrationId = (await rl.question(`Integration ID [${integrationIdDefault || "..."}]: `)).trim() || integrationIdDefault;
|
|
468
|
+
const apiKey = (await rl.question(`API key [${config.apiKey ? "unchanged" : "iak_..."}]: `)).trim() || config.apiKey;
|
|
469
|
+
const next = { ...config, baseUrl, integrationId, apiKey };
|
|
470
|
+
if (baseUrl && integrationId && apiKey) {
|
|
471
|
+
const client = new IntegrationClient({ baseUrl, integrationId, apiKey });
|
|
472
|
+
try {
|
|
473
|
+
const status = await client.getStatus();
|
|
474
|
+
process.stdout.write(`Connected: ${status.label} (${status.enabled ? "enabled" : "DISABLED"})\n`);
|
|
475
|
+
}
|
|
476
|
+
catch (err) {
|
|
477
|
+
process.stdout.write(`Connection test failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
478
|
+
const saveAnyway = (await rl.question("Save anyway? [y/N] ")).trim().toLowerCase();
|
|
479
|
+
if (saveAnyway !== "y" && saveAnyway !== "yes")
|
|
480
|
+
return 1;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
// Informed consent for the approval default: AUTO runs workspace tool
|
|
484
|
+
// actions (shell, write, edit) unattended — only outside-workspace paths
|
|
485
|
+
// still prompt. The current value is the preselected answer.
|
|
486
|
+
const approvalHint = next.approvalDefault === "auto" ? "Y/n" : "y/N";
|
|
487
|
+
const approval = (await rl.question(`Auto-approve tool actions inside the workspace (shell/write/edit run unattended; outside-workspace paths always ask)? [${approvalHint}]: `)).trim().toLowerCase();
|
|
488
|
+
if (["y", "yes"].includes(approval))
|
|
489
|
+
next.approvalDefault = "auto";
|
|
490
|
+
if (["n", "no"].includes(approval))
|
|
491
|
+
next.approvalDefault = "ask";
|
|
492
|
+
if (!isConfigured(next)) {
|
|
493
|
+
process.stderr.write("Configuration incomplete — base URL, integration ID, and API key are all required. Nothing was saved.\n");
|
|
494
|
+
return 1;
|
|
495
|
+
}
|
|
496
|
+
await saveConfig(next);
|
|
497
|
+
process.stdout.write(`Saved to ${configDir()}/config.json\n`);
|
|
498
|
+
return 0;
|
|
499
|
+
}
|
|
500
|
+
finally {
|
|
501
|
+
rl.close();
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
if (isMainModule(import.meta)) {
|
|
505
|
+
main()
|
|
506
|
+
.then((code) => process.exit(code))
|
|
507
|
+
.catch((err) => {
|
|
508
|
+
process.stderr.write(`${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
|
|
509
|
+
process.exit(1);
|
|
510
|
+
});
|
|
511
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { AguiEvent, AguiInputMessage, AguiRunRequest, AguiToolDef, IntegrationStatus, ModelInfo, RunStreamResult, SessionDetail, SessionSummary, WorkspaceEntry, WorkspaceFile, WorkspaceWriteResult } from "./types.js";
|
|
2
|
+
export interface IntegrationClientOptions {
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
integrationId: string;
|
|
5
|
+
apiKey: string;
|
|
6
|
+
/** Injectable fetch (tests). */
|
|
7
|
+
fetch?: typeof fetch;
|
|
8
|
+
}
|
|
9
|
+
/** Error thrown for non-2xx integration API responses. */
|
|
10
|
+
export declare class IntegrationApiError extends Error {
|
|
11
|
+
readonly status: number;
|
|
12
|
+
/** Machine-readable discriminator from the error body (e.g. `tool_call_expired`). */
|
|
13
|
+
readonly code?: string | undefined;
|
|
14
|
+
/** Additive machine-readable context (e.g. `{ expiredIds }`). */
|
|
15
|
+
readonly details?: Record<string, unknown> | undefined;
|
|
16
|
+
constructor(message: string, status: number,
|
|
17
|
+
/** Machine-readable discriminator from the error body (e.g. `tool_call_expired`). */
|
|
18
|
+
code?: string | undefined,
|
|
19
|
+
/** Additive machine-readable context (e.g. `{ expiredIds }`). */
|
|
20
|
+
details?: Record<string, unknown> | undefined);
|
|
21
|
+
}
|
|
22
|
+
/** Options for a run stream (AG-UI surface). */
|
|
23
|
+
export interface RunStreamOptions {
|
|
24
|
+
signal?: AbortSignal;
|
|
25
|
+
onEvent: (event: AguiEvent) => void;
|
|
26
|
+
/** Called as soon as the server reports the thread id (X-Thread-Id header). */
|
|
27
|
+
onThreadId?: (threadId: string) => void;
|
|
28
|
+
}
|
|
29
|
+
/** One client tool result as the local execution layer produces it. */
|
|
30
|
+
export interface ToolOutput {
|
|
31
|
+
tool_call_id: string;
|
|
32
|
+
output: string;
|
|
33
|
+
isError?: boolean;
|
|
34
|
+
images?: Array<{
|
|
35
|
+
data: string;
|
|
36
|
+
mimeType: string;
|
|
37
|
+
}>;
|
|
38
|
+
}
|
|
39
|
+
export declare class IntegrationClient {
|
|
40
|
+
readonly baseUrl: string;
|
|
41
|
+
readonly integrationId: string;
|
|
42
|
+
private readonly apiKey;
|
|
43
|
+
private readonly fetchImpl;
|
|
44
|
+
constructor(opts: IntegrationClientOptions);
|
|
45
|
+
private url;
|
|
46
|
+
private headers;
|
|
47
|
+
/**
|
|
48
|
+
* Start a run and stream its AG-UI events. The stream stays open through
|
|
49
|
+
* client-tool waits (heartbeats keep it alive), so it is the single line for
|
|
50
|
+
* the whole run: collect `CUSTOM_TOOL_CALL` events, execute the calls, then
|
|
51
|
+
* submit results with sendRunMessages — the continuation's events keep
|
|
52
|
+
* flowing on this same stream.
|
|
53
|
+
*
|
|
54
|
+
* The thread id arrives in the X-Thread-Id response header, before the first
|
|
55
|
+
* event, and is also latched through `onThreadId`.
|
|
56
|
+
*/
|
|
57
|
+
startRun(req: AguiRunRequest, opts: RunStreamOptions): Promise<RunStreamResult>;
|
|
58
|
+
/**
|
|
59
|
+
* Continue the thread's active run: user/system messages steer it, tool-role
|
|
60
|
+
* messages resume it (tool results are awaited by the agent). The server
|
|
61
|
+
* answers with a single SSE frame — INPUT_ACCEPTED, or INPUT_REJECTED
|
|
62
|
+
* (thrown as IntegrationApiError with the machine-readable `code`). Run
|
|
63
|
+
* events keep flowing on the run stream, never on this response.
|
|
64
|
+
*
|
|
65
|
+
* Returns the persisted message id for steers (absent for tool-result
|
|
66
|
+
* resumes) — the caller correlates the later QUEUED_MESSAGE_DELIVERED event
|
|
67
|
+
* with it.
|
|
68
|
+
*/
|
|
69
|
+
sendRunMessages(threadId: string, messages: AguiInputMessage[], opts?: {
|
|
70
|
+
tools?: AguiToolDef[];
|
|
71
|
+
signal?: AbortSignal;
|
|
72
|
+
}): Promise<string | undefined>;
|
|
73
|
+
getStatus(opts?: {
|
|
74
|
+
signal?: AbortSignal;
|
|
75
|
+
}): Promise<IntegrationStatus>;
|
|
76
|
+
/**
|
|
77
|
+
* Models this integration accepts (default first, `default: true` marks it).
|
|
78
|
+
* A forced-model integration returns exactly one entry — there is nothing to
|
|
79
|
+
* choose. Used by the `/model` picker.
|
|
80
|
+
*/
|
|
81
|
+
listModels(): Promise<ModelInfo[]>;
|
|
82
|
+
listSessions(): Promise<SessionSummary[]>;
|
|
83
|
+
getSession(threadId: string, opts?: {
|
|
84
|
+
history?: boolean;
|
|
85
|
+
historyLimit?: number;
|
|
86
|
+
}): Promise<SessionDetail>;
|
|
87
|
+
patchSessionTitle(threadId: string, title: string, opts?: {
|
|
88
|
+
signal?: AbortSignal;
|
|
89
|
+
}): Promise<void>;
|
|
90
|
+
deleteSession(threadId: string): Promise<void>;
|
|
91
|
+
stopSession(threadId: string): Promise<void>;
|
|
92
|
+
compactSession(threadId: string, focus?: string): Promise<void>;
|
|
93
|
+
/** GET /integrations/<id>/workspace?path= — list a workspace directory. */
|
|
94
|
+
workspaceList(path: string, opts?: {
|
|
95
|
+
signal?: AbortSignal;
|
|
96
|
+
}): Promise<WorkspaceEntry[]>;
|
|
97
|
+
/** GET /integrations/<id>/workspace/read?path= — read one file (base64 when binary). */
|
|
98
|
+
workspaceRead(path: string, opts?: {
|
|
99
|
+
signal?: AbortSignal;
|
|
100
|
+
}): Promise<WorkspaceFile>;
|
|
101
|
+
/** PUT /integrations/<id>/workspace/write — create or overwrite one file. */
|
|
102
|
+
workspaceWrite(path: string, content: string, binary: boolean, opts?: {
|
|
103
|
+
signal?: AbortSignal;
|
|
104
|
+
}): Promise<WorkspaceWriteResult>;
|
|
105
|
+
/**
|
|
106
|
+
* Subscribe to a session's AG-UI event stream (SSE). Resolves when the
|
|
107
|
+
* stream ends (server close or network drop) — the caller reconnects with
|
|
108
|
+
* backoff. AbortError propagates so a deliberate unsubscribe is
|
|
109
|
+
* distinguishable from a drop.
|
|
110
|
+
*
|
|
111
|
+
* Events are delivered verbatim: `MESSAGES_SNAPSHOT` first (transcript +
|
|
112
|
+
* status), then live AG-UI events including `TOOL_CALL_RESULT` for
|
|
113
|
+
* client-executed calls (an error result means the server released the call).
|
|
114
|
+
*/
|
|
115
|
+
openSessionEvents(threadId: string, opts: {
|
|
116
|
+
signal?: AbortSignal;
|
|
117
|
+
onEvent: (event: Record<string, unknown>) => void;
|
|
118
|
+
}): Promise<void>;
|
|
119
|
+
private json;
|
|
120
|
+
/** Build an IntegrationApiError from a non-OK response, preserving the
|
|
121
|
+
* machine-readable `error.code` (e.g. `tool_call_expired`). */
|
|
122
|
+
private apiError;
|
|
123
|
+
}
|