@link-assistant/agent 0.0.8 → 0.0.11
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/EXAMPLES.md +80 -1
- package/MODELS.md +72 -24
- package/README.md +95 -2
- package/TOOLS.md +20 -0
- package/package.json +36 -2
- package/src/agent/agent.ts +68 -54
- package/src/auth/claude-oauth.ts +426 -0
- package/src/auth/index.ts +28 -26
- package/src/auth/plugins.ts +876 -0
- package/src/bun/index.ts +53 -43
- package/src/bus/global.ts +5 -5
- package/src/bus/index.ts +59 -53
- package/src/cli/bootstrap.js +12 -12
- package/src/cli/bootstrap.ts +6 -6
- package/src/cli/cmd/agent.ts +97 -92
- package/src/cli/cmd/auth.ts +468 -0
- package/src/cli/cmd/cmd.ts +2 -2
- package/src/cli/cmd/export.ts +41 -41
- package/src/cli/cmd/mcp.ts +210 -53
- package/src/cli/cmd/models.ts +30 -29
- package/src/cli/cmd/run.ts +269 -213
- package/src/cli/cmd/stats.ts +185 -146
- package/src/cli/error.ts +17 -13
- package/src/cli/ui.ts +78 -0
- package/src/command/index.ts +26 -26
- package/src/config/config.ts +528 -288
- package/src/config/markdown.ts +15 -15
- package/src/file/ripgrep.ts +201 -169
- package/src/file/time.ts +21 -18
- package/src/file/watcher.ts +51 -42
- package/src/file.ts +1 -1
- package/src/flag/flag.ts +26 -11
- package/src/format/formatter.ts +206 -162
- package/src/format/index.ts +61 -61
- package/src/global/index.ts +21 -21
- package/src/id/id.ts +47 -33
- package/src/index.js +554 -332
- package/src/json-standard/index.ts +173 -0
- package/src/mcp/index.ts +135 -128
- package/src/patch/index.ts +336 -267
- package/src/project/bootstrap.ts +15 -15
- package/src/project/instance.ts +43 -36
- package/src/project/project.ts +47 -47
- package/src/project/state.ts +37 -33
- package/src/provider/models-macro.ts +5 -5
- package/src/provider/models.ts +32 -32
- package/src/provider/opencode.js +19 -19
- package/src/provider/provider.ts +518 -277
- package/src/provider/transform.ts +143 -102
- package/src/server/project.ts +21 -21
- package/src/server/server.ts +111 -105
- package/src/session/agent.js +66 -60
- package/src/session/compaction.ts +136 -111
- package/src/session/index.ts +189 -156
- package/src/session/message-v2.ts +312 -268
- package/src/session/message.ts +73 -57
- package/src/session/processor.ts +180 -166
- package/src/session/prompt.ts +678 -533
- package/src/session/retry.ts +26 -23
- package/src/session/revert.ts +76 -62
- package/src/session/status.ts +26 -26
- package/src/session/summary.ts +97 -76
- package/src/session/system.ts +77 -63
- package/src/session/todo.ts +22 -16
- package/src/snapshot/index.ts +92 -76
- package/src/storage/storage.ts +157 -120
- package/src/tool/bash.ts +116 -106
- package/src/tool/batch.ts +73 -59
- package/src/tool/codesearch.ts +60 -53
- package/src/tool/edit.ts +319 -263
- package/src/tool/glob.ts +32 -28
- package/src/tool/grep.ts +72 -53
- package/src/tool/invalid.ts +7 -7
- package/src/tool/ls.ts +77 -64
- package/src/tool/multiedit.ts +30 -21
- package/src/tool/patch.ts +121 -94
- package/src/tool/read.ts +140 -122
- package/src/tool/registry.ts +38 -38
- package/src/tool/task.ts +93 -60
- package/src/tool/todo.ts +16 -16
- package/src/tool/tool.ts +45 -36
- package/src/tool/webfetch.ts +97 -74
- package/src/tool/websearch.ts +78 -64
- package/src/tool/write.ts +21 -15
- package/src/util/binary.ts +27 -19
- package/src/util/context.ts +8 -8
- package/src/util/defer.ts +7 -5
- package/src/util/error.ts +24 -19
- package/src/util/eventloop.ts +16 -10
- package/src/util/filesystem.ts +37 -33
- package/src/util/fn.ts +11 -8
- package/src/util/iife.ts +1 -1
- package/src/util/keybind.ts +44 -44
- package/src/util/lazy.ts +7 -7
- package/src/util/locale.ts +20 -16
- package/src/util/lock.ts +43 -38
- package/src/util/log.ts +95 -85
- package/src/util/queue.ts +8 -8
- package/src/util/rpc.ts +35 -23
- package/src/util/scrap.ts +4 -4
- package/src/util/signal.ts +5 -5
- package/src/util/timeout.ts +6 -6
- package/src/util/token.ts +2 -2
- package/src/util/wildcard.ts +38 -27
package/src/index.js
CHANGED
|
@@ -1,371 +1,593 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
-
import { Server } from './server/server.ts'
|
|
4
|
-
import { Instance } from './project/instance.ts'
|
|
5
|
-
import { Log } from './util/log.ts'
|
|
6
|
-
import { Bus } from './bus/index.ts'
|
|
7
|
-
import { Session } from './session/index.ts'
|
|
8
|
-
import { SessionPrompt } from './session/prompt.ts'
|
|
9
|
-
|
|
10
|
-
import yargs from 'yargs'
|
|
11
|
-
import { hideBin } from 'yargs/helpers'
|
|
12
|
-
|
|
13
|
-
|
|
3
|
+
import { Server } from './server/server.ts';
|
|
4
|
+
import { Instance } from './project/instance.ts';
|
|
5
|
+
import { Log } from './util/log.ts';
|
|
6
|
+
import { Bus } from './bus/index.ts';
|
|
7
|
+
import { Session } from './session/index.ts';
|
|
8
|
+
import { SessionPrompt } from './session/prompt.ts';
|
|
9
|
+
// EOL is reserved for future use
|
|
10
|
+
import yargs from 'yargs';
|
|
11
|
+
import { hideBin } from 'yargs/helpers';
|
|
12
|
+
import {
|
|
13
|
+
createEventHandler,
|
|
14
|
+
isValidJsonStandard,
|
|
15
|
+
} from './json-standard/index.ts';
|
|
16
|
+
import { McpCommand } from './cli/cmd/mcp.ts';
|
|
17
|
+
import { AuthCommand } from './cli/cmd/auth.ts';
|
|
18
|
+
import { Flag } from './flag/flag.ts';
|
|
19
|
+
|
|
20
|
+
// Track if any errors occurred during execution
|
|
21
|
+
let hasError = false;
|
|
22
|
+
|
|
23
|
+
// Install global error handlers to ensure non-zero exit codes
|
|
24
|
+
process.on('uncaughtException', (error) => {
|
|
25
|
+
hasError = true;
|
|
26
|
+
console.error(
|
|
27
|
+
JSON.stringify(
|
|
28
|
+
{
|
|
29
|
+
type: 'error',
|
|
30
|
+
errorType: error.name || 'UncaughtException',
|
|
31
|
+
message: error.message,
|
|
32
|
+
stack: error.stack,
|
|
33
|
+
},
|
|
34
|
+
null,
|
|
35
|
+
2
|
|
36
|
+
)
|
|
37
|
+
);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
process.on('unhandledRejection', (reason, _promise) => {
|
|
42
|
+
hasError = true;
|
|
43
|
+
console.error(
|
|
44
|
+
JSON.stringify(
|
|
45
|
+
{
|
|
46
|
+
type: 'error',
|
|
47
|
+
errorType: 'UnhandledRejection',
|
|
48
|
+
message: reason?.message || String(reason),
|
|
49
|
+
stack: reason?.stack,
|
|
50
|
+
},
|
|
51
|
+
null,
|
|
52
|
+
2
|
|
53
|
+
)
|
|
54
|
+
);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
function readStdin() {
|
|
14
59
|
return new Promise((resolve, reject) => {
|
|
15
|
-
let data = ''
|
|
16
|
-
const onData = chunk => {
|
|
17
|
-
data += chunk
|
|
18
|
-
}
|
|
60
|
+
let data = '';
|
|
61
|
+
const onData = (chunk) => {
|
|
62
|
+
data += chunk;
|
|
63
|
+
};
|
|
19
64
|
const onEnd = () => {
|
|
20
|
-
cleanup()
|
|
21
|
-
resolve(data)
|
|
65
|
+
cleanup();
|
|
66
|
+
resolve(data);
|
|
67
|
+
};
|
|
68
|
+
const onError = (err) => {
|
|
69
|
+
cleanup();
|
|
70
|
+
reject(err);
|
|
71
|
+
};
|
|
72
|
+
const cleanup = () => {
|
|
73
|
+
process.stdin.removeListener('data', onData);
|
|
74
|
+
process.stdin.removeListener('end', onEnd);
|
|
75
|
+
process.stdin.removeListener('error', onError);
|
|
76
|
+
};
|
|
77
|
+
process.stdin.on('data', onData);
|
|
78
|
+
process.stdin.on('end', onEnd);
|
|
79
|
+
process.stdin.on('error', onError);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function runAgentMode(argv) {
|
|
84
|
+
// Set verbose mode if requested via CLI flag
|
|
85
|
+
if (argv.verbose) {
|
|
86
|
+
Flag.setVerbose(true);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Parse model argument (handle model IDs with slashes like groq/qwen/qwen3-32b)
|
|
90
|
+
const modelParts = argv.model.split('/');
|
|
91
|
+
let providerID = modelParts[0] || 'opencode';
|
|
92
|
+
let modelID = modelParts.slice(1).join('/') || 'grok-code';
|
|
93
|
+
|
|
94
|
+
// Handle --use-existing-claude-oauth option
|
|
95
|
+
// This reads OAuth credentials from ~/.claude/.credentials.json (Claude Code CLI)
|
|
96
|
+
// For new authentication, use: agent auth login (select Anthropic > Claude Pro/Max)
|
|
97
|
+
if (argv['use-existing-claude-oauth']) {
|
|
98
|
+
// Import ClaudeOAuth to check for credentials from Claude Code CLI
|
|
99
|
+
const { ClaudeOAuth } = await import('./auth/claude-oauth.ts');
|
|
100
|
+
const creds = await ClaudeOAuth.getCredentials();
|
|
101
|
+
|
|
102
|
+
if (!creds?.accessToken) {
|
|
103
|
+
console.error(
|
|
104
|
+
JSON.stringify({
|
|
105
|
+
type: 'error',
|
|
106
|
+
errorType: 'AuthenticationError',
|
|
107
|
+
message:
|
|
108
|
+
'No Claude OAuth credentials found in ~/.claude/.credentials.json. Either authenticate with Claude Code CLI first, or use: agent auth login (select Anthropic > Claude Pro/Max)',
|
|
109
|
+
})
|
|
110
|
+
);
|
|
111
|
+
process.exit(1);
|
|
22
112
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
113
|
+
|
|
114
|
+
// Set environment variable for the provider to use
|
|
115
|
+
process.env.CLAUDE_CODE_OAUTH_TOKEN = creds.accessToken;
|
|
116
|
+
|
|
117
|
+
// If user specified a model, use it with claude-oauth provider
|
|
118
|
+
// If not, use claude-oauth/claude-sonnet-4-5 as default
|
|
119
|
+
if (providerID === 'opencode' && modelID === 'grok-code') {
|
|
120
|
+
providerID = 'claude-oauth';
|
|
121
|
+
modelID = 'claude-sonnet-4-5';
|
|
122
|
+
} else if (!['claude-oauth', 'anthropic'].includes(providerID)) {
|
|
123
|
+
// If user specified a different provider, warn them
|
|
124
|
+
console.error(
|
|
125
|
+
JSON.stringify({
|
|
126
|
+
type: 'warning',
|
|
127
|
+
message: `--use-existing-claude-oauth is set but model uses provider "${providerID}". Using OAuth credentials anyway.`,
|
|
128
|
+
})
|
|
129
|
+
);
|
|
130
|
+
providerID = 'claude-oauth';
|
|
26
131
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Validate and get JSON standard
|
|
135
|
+
const jsonStandard = argv['json-standard'];
|
|
136
|
+
if (!isValidJsonStandard(jsonStandard)) {
|
|
137
|
+
console.error(
|
|
138
|
+
`Invalid JSON standard: ${jsonStandard}. Use "opencode" or "claude".`
|
|
139
|
+
);
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Read system message files
|
|
144
|
+
let systemMessage = argv['system-message'];
|
|
145
|
+
let appendSystemMessage = argv['append-system-message'];
|
|
146
|
+
|
|
147
|
+
if (argv['system-message-file']) {
|
|
148
|
+
const resolvedPath = require('path').resolve(
|
|
149
|
+
process.cwd(),
|
|
150
|
+
argv['system-message-file']
|
|
151
|
+
);
|
|
152
|
+
const file = Bun.file(resolvedPath);
|
|
153
|
+
if (!(await file.exists())) {
|
|
154
|
+
console.error(
|
|
155
|
+
`System message file not found: ${argv['system-message-file']}`
|
|
156
|
+
);
|
|
157
|
+
process.exit(1);
|
|
31
158
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
159
|
+
systemMessage = await file.text();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (argv['append-system-message-file']) {
|
|
163
|
+
const resolvedPath = require('path').resolve(
|
|
164
|
+
process.cwd(),
|
|
165
|
+
argv['append-system-message-file']
|
|
166
|
+
);
|
|
167
|
+
const file = Bun.file(resolvedPath);
|
|
168
|
+
if (!(await file.exists())) {
|
|
169
|
+
console.error(
|
|
170
|
+
`Append system message file not found: ${argv['append-system-message-file']}`
|
|
171
|
+
);
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
appendSystemMessage = await file.text();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Initialize logging to redirect to log file instead of stderr
|
|
178
|
+
// This prevents log messages from mixing with JSON output
|
|
179
|
+
// In verbose mode, print to stderr for debugging
|
|
180
|
+
await Log.init({
|
|
181
|
+
print: Flag.OPENCODE_VERBOSE, // Print to stderr only in verbose mode
|
|
182
|
+
level: Flag.OPENCODE_VERBOSE ? 'DEBUG' : 'INFO',
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Read input from stdin
|
|
186
|
+
const input = await readStdin();
|
|
187
|
+
const trimmedInput = input.trim();
|
|
188
|
+
|
|
189
|
+
// Try to parse as JSON, if it fails treat it as plain text message
|
|
190
|
+
let request;
|
|
191
|
+
try {
|
|
192
|
+
request = JSON.parse(trimmedInput);
|
|
193
|
+
} catch (_e) {
|
|
194
|
+
// Not JSON, treat as plain text message
|
|
195
|
+
request = {
|
|
196
|
+
message: trimmedInput,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Wrap in Instance.provide for OpenCode infrastructure
|
|
201
|
+
await Instance.provide({
|
|
202
|
+
directory: process.cwd(),
|
|
203
|
+
fn: async () => {
|
|
204
|
+
if (argv.server) {
|
|
205
|
+
// SERVER MODE: Start server and communicate via HTTP
|
|
206
|
+
await runServerMode(
|
|
207
|
+
request,
|
|
208
|
+
providerID,
|
|
209
|
+
modelID,
|
|
210
|
+
systemMessage,
|
|
211
|
+
appendSystemMessage,
|
|
212
|
+
jsonStandard
|
|
213
|
+
);
|
|
214
|
+
} else {
|
|
215
|
+
// DIRECT MODE: Run everything in single process
|
|
216
|
+
await runDirectMode(
|
|
217
|
+
request,
|
|
218
|
+
providerID,
|
|
219
|
+
modelID,
|
|
220
|
+
systemMessage,
|
|
221
|
+
appendSystemMessage,
|
|
222
|
+
jsonStandard
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Explicitly exit to ensure process terminates
|
|
229
|
+
process.exit(hasError ? 1 : 0);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function runServerMode(
|
|
233
|
+
request,
|
|
234
|
+
providerID,
|
|
235
|
+
modelID,
|
|
236
|
+
systemMessage,
|
|
237
|
+
appendSystemMessage,
|
|
238
|
+
jsonStandard
|
|
239
|
+
) {
|
|
240
|
+
// Start server like OpenCode does
|
|
241
|
+
const server = Server.listen({ port: 0, hostname: '127.0.0.1' });
|
|
242
|
+
let unsub = null;
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
// Create a session
|
|
246
|
+
const createRes = await fetch(
|
|
247
|
+
`http://${server.hostname}:${server.port}/session`,
|
|
248
|
+
{
|
|
249
|
+
method: 'POST',
|
|
250
|
+
headers: { 'Content-Type': 'application/json' },
|
|
251
|
+
body: JSON.stringify({}),
|
|
252
|
+
}
|
|
253
|
+
);
|
|
254
|
+
const session = await createRes.json();
|
|
255
|
+
const sessionID = session.id;
|
|
256
|
+
|
|
257
|
+
if (!sessionID) {
|
|
258
|
+
throw new Error('Failed to create session');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Create event handler for the selected JSON standard
|
|
262
|
+
const eventHandler = createEventHandler(jsonStandard, sessionID);
|
|
263
|
+
|
|
264
|
+
// Subscribe to all bus events and output in selected format
|
|
265
|
+
const eventPromise = new Promise((resolve) => {
|
|
266
|
+
unsub = Bus.subscribeAll((event) => {
|
|
267
|
+
// Output events in selected JSON format
|
|
268
|
+
if (event.type === 'message.part.updated') {
|
|
269
|
+
const part = event.properties.part;
|
|
270
|
+
if (part.sessionID !== sessionID) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Output different event types
|
|
275
|
+
if (part.type === 'step-start') {
|
|
276
|
+
eventHandler.output({
|
|
277
|
+
type: 'step_start',
|
|
278
|
+
timestamp: Date.now(),
|
|
279
|
+
sessionID,
|
|
280
|
+
part,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (part.type === 'step-finish') {
|
|
285
|
+
eventHandler.output({
|
|
286
|
+
type: 'step_finish',
|
|
287
|
+
timestamp: Date.now(),
|
|
288
|
+
sessionID,
|
|
289
|
+
part,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (part.type === 'text' && part.time?.end) {
|
|
294
|
+
eventHandler.output({
|
|
295
|
+
type: 'text',
|
|
296
|
+
timestamp: Date.now(),
|
|
297
|
+
sessionID,
|
|
298
|
+
part,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (part.type === 'tool' && part.state.status === 'completed') {
|
|
303
|
+
eventHandler.output({
|
|
304
|
+
type: 'tool_use',
|
|
305
|
+
timestamp: Date.now(),
|
|
306
|
+
sessionID,
|
|
307
|
+
part,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Handle session idle to know when to stop
|
|
313
|
+
if (
|
|
314
|
+
event.type === 'session.idle' &&
|
|
315
|
+
event.properties.sessionID === sessionID
|
|
316
|
+
) {
|
|
317
|
+
resolve();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Handle errors
|
|
321
|
+
if (event.type === 'session.error') {
|
|
322
|
+
const props = event.properties;
|
|
323
|
+
if (props.sessionID !== sessionID || !props.error) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
hasError = true;
|
|
327
|
+
eventHandler.output({
|
|
328
|
+
type: 'error',
|
|
329
|
+
timestamp: Date.now(),
|
|
330
|
+
sessionID,
|
|
331
|
+
error: props.error,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// Send message to session with specified model (default: opencode/grok-code)
|
|
338
|
+
const message = request.message || 'hi';
|
|
339
|
+
const parts = [{ type: 'text', text: message }];
|
|
340
|
+
|
|
341
|
+
// Start the prompt (don't wait for response, events come via Bus)
|
|
342
|
+
fetch(
|
|
343
|
+
`http://${server.hostname}:${server.port}/session/${sessionID}/message`,
|
|
344
|
+
{
|
|
345
|
+
method: 'POST',
|
|
346
|
+
headers: { 'Content-Type': 'application/json' },
|
|
347
|
+
body: JSON.stringify({
|
|
348
|
+
parts,
|
|
349
|
+
model: {
|
|
350
|
+
providerID,
|
|
351
|
+
modelID,
|
|
352
|
+
},
|
|
353
|
+
system: systemMessage,
|
|
354
|
+
appendSystem: appendSystemMessage,
|
|
355
|
+
}),
|
|
356
|
+
}
|
|
357
|
+
).catch((error) => {
|
|
358
|
+
hasError = true;
|
|
359
|
+
eventHandler.output({
|
|
360
|
+
type: 'error',
|
|
361
|
+
timestamp: Date.now(),
|
|
362
|
+
sessionID,
|
|
363
|
+
error: error instanceof Error ? error.message : String(error),
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// Wait for session to become idle
|
|
368
|
+
await eventPromise;
|
|
369
|
+
} finally {
|
|
370
|
+
// Always clean up resources
|
|
371
|
+
if (unsub) {
|
|
372
|
+
unsub();
|
|
373
|
+
}
|
|
374
|
+
server.stop();
|
|
375
|
+
await Instance.dispose();
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async function runDirectMode(
|
|
380
|
+
request,
|
|
381
|
+
providerID,
|
|
382
|
+
modelID,
|
|
383
|
+
systemMessage,
|
|
384
|
+
appendSystemMessage,
|
|
385
|
+
jsonStandard
|
|
386
|
+
) {
|
|
387
|
+
// DIRECT MODE: Run in single process without server
|
|
388
|
+
let unsub = null;
|
|
389
|
+
|
|
390
|
+
try {
|
|
391
|
+
// Create a session directly
|
|
392
|
+
const session = await Session.createNext({
|
|
393
|
+
directory: process.cwd(),
|
|
394
|
+
});
|
|
395
|
+
const sessionID = session.id;
|
|
396
|
+
|
|
397
|
+
// Create event handler for the selected JSON standard
|
|
398
|
+
const eventHandler = createEventHandler(jsonStandard, sessionID);
|
|
399
|
+
|
|
400
|
+
// Subscribe to all bus events and output in selected format
|
|
401
|
+
const eventPromise = new Promise((resolve) => {
|
|
402
|
+
unsub = Bus.subscribeAll((event) => {
|
|
403
|
+
// Output events in selected JSON format
|
|
404
|
+
if (event.type === 'message.part.updated') {
|
|
405
|
+
const part = event.properties.part;
|
|
406
|
+
if (part.sessionID !== sessionID) {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Output different event types
|
|
411
|
+
if (part.type === 'step-start') {
|
|
412
|
+
eventHandler.output({
|
|
413
|
+
type: 'step_start',
|
|
414
|
+
timestamp: Date.now(),
|
|
415
|
+
sessionID,
|
|
416
|
+
part,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (part.type === 'step-finish') {
|
|
421
|
+
eventHandler.output({
|
|
422
|
+
type: 'step_finish',
|
|
423
|
+
timestamp: Date.now(),
|
|
424
|
+
sessionID,
|
|
425
|
+
part,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (part.type === 'text' && part.time?.end) {
|
|
430
|
+
eventHandler.output({
|
|
431
|
+
type: 'text',
|
|
432
|
+
timestamp: Date.now(),
|
|
433
|
+
sessionID,
|
|
434
|
+
part,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (part.type === 'tool' && part.state.status === 'completed') {
|
|
439
|
+
eventHandler.output({
|
|
440
|
+
type: 'tool_use',
|
|
441
|
+
timestamp: Date.now(),
|
|
442
|
+
sessionID,
|
|
443
|
+
part,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Handle session idle to know when to stop
|
|
449
|
+
if (
|
|
450
|
+
event.type === 'session.idle' &&
|
|
451
|
+
event.properties.sessionID === sessionID
|
|
452
|
+
) {
|
|
453
|
+
resolve();
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Handle errors
|
|
457
|
+
if (event.type === 'session.error') {
|
|
458
|
+
const props = event.properties;
|
|
459
|
+
if (props.sessionID !== sessionID || !props.error) {
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
hasError = true;
|
|
463
|
+
eventHandler.output({
|
|
464
|
+
type: 'error',
|
|
465
|
+
timestamp: Date.now(),
|
|
466
|
+
sessionID,
|
|
467
|
+
error: props.error,
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// Send message to session directly
|
|
474
|
+
const message = request.message || 'hi';
|
|
475
|
+
const parts = [{ type: 'text', text: message }];
|
|
476
|
+
|
|
477
|
+
// Start the prompt directly without HTTP
|
|
478
|
+
SessionPrompt.prompt({
|
|
479
|
+
sessionID,
|
|
480
|
+
parts,
|
|
481
|
+
model: {
|
|
482
|
+
providerID,
|
|
483
|
+
modelID,
|
|
484
|
+
},
|
|
485
|
+
system: systemMessage,
|
|
486
|
+
appendSystem: appendSystemMessage,
|
|
487
|
+
}).catch((error) => {
|
|
488
|
+
hasError = true;
|
|
489
|
+
eventHandler.output({
|
|
490
|
+
type: 'error',
|
|
491
|
+
timestamp: Date.now(),
|
|
492
|
+
sessionID,
|
|
493
|
+
error: error instanceof Error ? error.message : String(error),
|
|
494
|
+
});
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
// Wait for session to become idle
|
|
498
|
+
await eventPromise;
|
|
499
|
+
} finally {
|
|
500
|
+
// Always clean up resources
|
|
501
|
+
if (unsub) {
|
|
502
|
+
unsub();
|
|
503
|
+
}
|
|
504
|
+
await Instance.dispose();
|
|
505
|
+
}
|
|
36
506
|
}
|
|
37
507
|
|
|
38
508
|
async function main() {
|
|
39
509
|
try {
|
|
40
|
-
// Parse command line arguments
|
|
510
|
+
// Parse command line arguments with subcommands
|
|
41
511
|
const argv = await yargs(hideBin(process.argv))
|
|
512
|
+
.scriptName('agent')
|
|
513
|
+
.usage('$0 [command] [options]')
|
|
514
|
+
// MCP subcommand
|
|
515
|
+
.command(McpCommand)
|
|
516
|
+
// Auth subcommand
|
|
517
|
+
.command(AuthCommand)
|
|
518
|
+
// Default run mode (when piping stdin)
|
|
42
519
|
.option('model', {
|
|
43
520
|
type: 'string',
|
|
44
521
|
description: 'Model to use in format providerID/modelID',
|
|
45
|
-
default: 'opencode/grok-code'
|
|
522
|
+
default: 'opencode/grok-code',
|
|
523
|
+
})
|
|
524
|
+
.option('json-standard', {
|
|
525
|
+
type: 'string',
|
|
526
|
+
description:
|
|
527
|
+
'JSON output format standard: "opencode" (default) or "claude" (experimental)',
|
|
528
|
+
default: 'opencode',
|
|
529
|
+
choices: ['opencode', 'claude'],
|
|
46
530
|
})
|
|
47
531
|
.option('system-message', {
|
|
48
532
|
type: 'string',
|
|
49
|
-
description: 'Full override of the system message'
|
|
533
|
+
description: 'Full override of the system message',
|
|
50
534
|
})
|
|
51
535
|
.option('system-message-file', {
|
|
52
536
|
type: 'string',
|
|
53
|
-
description: 'Full override of the system message from file'
|
|
537
|
+
description: 'Full override of the system message from file',
|
|
54
538
|
})
|
|
55
539
|
.option('append-system-message', {
|
|
56
540
|
type: 'string',
|
|
57
|
-
description: 'Append to the default system message'
|
|
541
|
+
description: 'Append to the default system message',
|
|
58
542
|
})
|
|
59
543
|
.option('append-system-message-file', {
|
|
60
544
|
type: 'string',
|
|
61
|
-
description: 'Append to the default system message from file'
|
|
545
|
+
description: 'Append to the default system message from file',
|
|
62
546
|
})
|
|
63
547
|
.option('server', {
|
|
64
548
|
type: 'boolean',
|
|
65
549
|
description: 'Run in server mode (default)',
|
|
66
|
-
default: true
|
|
550
|
+
default: true,
|
|
67
551
|
})
|
|
68
|
-
.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const resolvedPath = require('path').resolve(process.cwd(), argv['system-message-file'])
|
|
82
|
-
const file = Bun.file(resolvedPath)
|
|
83
|
-
if (!(await file.exists())) {
|
|
84
|
-
console.error(`System message file not found: ${argv['system-message-file']}`)
|
|
85
|
-
process.exit(1)
|
|
86
|
-
}
|
|
87
|
-
systemMessage = await file.text()
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (argv['append-system-message-file']) {
|
|
91
|
-
const resolvedPath = require('path').resolve(process.cwd(), argv['append-system-message-file'])
|
|
92
|
-
const file = Bun.file(resolvedPath)
|
|
93
|
-
if (!(await file.exists())) {
|
|
94
|
-
console.error(`Append system message file not found: ${argv['append-system-message-file']}`)
|
|
95
|
-
process.exit(1)
|
|
96
|
-
}
|
|
97
|
-
appendSystemMessage = await file.text()
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Initialize logging to redirect to log file instead of stderr
|
|
101
|
-
// This prevents log messages from mixing with JSON output
|
|
102
|
-
await Log.init({
|
|
103
|
-
print: false, // Don't print to stderr
|
|
104
|
-
level: 'INFO'
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
// Read input from stdin
|
|
108
|
-
const input = await readStdin()
|
|
109
|
-
const trimmedInput = input.trim()
|
|
110
|
-
|
|
111
|
-
// Try to parse as JSON, if it fails treat it as plain text message
|
|
112
|
-
let request
|
|
113
|
-
try {
|
|
114
|
-
request = JSON.parse(trimmedInput)
|
|
115
|
-
} catch (e) {
|
|
116
|
-
// Not JSON, treat as plain text message
|
|
117
|
-
request = {
|
|
118
|
-
message: trimmedInput
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Wrap in Instance.provide for OpenCode infrastructure
|
|
123
|
-
await Instance.provide({
|
|
124
|
-
directory: process.cwd(),
|
|
125
|
-
fn: async () => {
|
|
126
|
-
if (argv.server) {
|
|
127
|
-
// SERVER MODE: Start server and communicate via HTTP
|
|
128
|
-
await runServerMode()
|
|
129
|
-
} else {
|
|
130
|
-
// DIRECT MODE: Run everything in single process
|
|
131
|
-
await runDirectMode()
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
async function runServerMode() {
|
|
137
|
-
// Start server like OpenCode does
|
|
138
|
-
const server = Server.listen({ port: 0, hostname: "127.0.0.1" })
|
|
139
|
-
let unsub = null
|
|
140
|
-
|
|
141
|
-
try {
|
|
142
|
-
// Create a session
|
|
143
|
-
const createRes = await fetch(`http://${server.hostname}:${server.port}/session`, {
|
|
144
|
-
method: 'POST',
|
|
145
|
-
headers: { 'Content-Type': 'application/json' },
|
|
146
|
-
body: JSON.stringify({})
|
|
147
|
-
})
|
|
148
|
-
const session = await createRes.json()
|
|
149
|
-
const sessionID = session.id
|
|
150
|
-
|
|
151
|
-
if (!sessionID) {
|
|
152
|
-
throw new Error("Failed to create session")
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Subscribe to all bus events to output them in OpenCode format
|
|
156
|
-
const eventPromise = new Promise((resolve) => {
|
|
157
|
-
unsub = Bus.subscribeAll((event) => {
|
|
158
|
-
// Output events in OpenCode JSON format
|
|
159
|
-
if (event.type === 'message.part.updated') {
|
|
160
|
-
const part = event.properties.part
|
|
161
|
-
if (part.sessionID !== sessionID) return
|
|
162
|
-
|
|
163
|
-
// Output different event types (pretty-printed for readability)
|
|
164
|
-
if (part.type === 'step-start') {
|
|
165
|
-
process.stdout.write(JSON.stringify({
|
|
166
|
-
type: 'step_start',
|
|
167
|
-
timestamp: Date.now(),
|
|
168
|
-
sessionID,
|
|
169
|
-
part
|
|
170
|
-
}, null, 2) + EOL)
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (part.type === 'step-finish') {
|
|
174
|
-
process.stdout.write(JSON.stringify({
|
|
175
|
-
type: 'step_finish',
|
|
176
|
-
timestamp: Date.now(),
|
|
177
|
-
sessionID,
|
|
178
|
-
part
|
|
179
|
-
}, null, 2) + EOL)
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (part.type === 'text' && part.time?.end) {
|
|
183
|
-
process.stdout.write(JSON.stringify({
|
|
184
|
-
type: 'text',
|
|
185
|
-
timestamp: Date.now(),
|
|
186
|
-
sessionID,
|
|
187
|
-
part
|
|
188
|
-
}, null, 2) + EOL)
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if (part.type === 'tool' && part.state.status === 'completed') {
|
|
192
|
-
process.stdout.write(JSON.stringify({
|
|
193
|
-
type: 'tool_use',
|
|
194
|
-
timestamp: Date.now(),
|
|
195
|
-
sessionID,
|
|
196
|
-
part
|
|
197
|
-
}, null, 2) + EOL)
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Handle session idle to know when to stop
|
|
202
|
-
if (event.type === 'session.idle' && event.properties.sessionID === sessionID) {
|
|
203
|
-
resolve()
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Handle errors
|
|
207
|
-
if (event.type === 'session.error') {
|
|
208
|
-
const props = event.properties
|
|
209
|
-
if (props.sessionID !== sessionID || !props.error) return
|
|
210
|
-
process.stdout.write(JSON.stringify({
|
|
211
|
-
type: 'error',
|
|
212
|
-
timestamp: Date.now(),
|
|
213
|
-
sessionID,
|
|
214
|
-
error: props.error
|
|
215
|
-
}, null, 2) + EOL)
|
|
216
|
-
}
|
|
217
|
-
})
|
|
218
|
-
})
|
|
219
|
-
|
|
220
|
-
// Send message to session with specified model (default: opencode/grok-code)
|
|
221
|
-
const message = request.message || "hi"
|
|
222
|
-
const parts = [{ type: "text", text: message }]
|
|
223
|
-
|
|
224
|
-
// Start the prompt (don't wait for response, events come via Bus)
|
|
225
|
-
fetch(`http://${server.hostname}:${server.port}/session/${sessionID}/message`, {
|
|
226
|
-
method: 'POST',
|
|
227
|
-
headers: { 'Content-Type': 'application/json' },
|
|
228
|
-
body: JSON.stringify({
|
|
229
|
-
parts,
|
|
230
|
-
model: {
|
|
231
|
-
providerID,
|
|
232
|
-
modelID
|
|
233
|
-
},
|
|
234
|
-
system: systemMessage,
|
|
235
|
-
appendSystem: appendSystemMessage
|
|
236
|
-
})
|
|
237
|
-
}).catch(() => {
|
|
238
|
-
// Ignore errors, we're listening to events
|
|
239
|
-
})
|
|
240
|
-
|
|
241
|
-
// Wait for session to become idle
|
|
242
|
-
await eventPromise
|
|
243
|
-
} finally {
|
|
244
|
-
// Always clean up resources
|
|
245
|
-
if (unsub) unsub()
|
|
246
|
-
server.stop()
|
|
247
|
-
await Instance.dispose()
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
async function runDirectMode() {
|
|
252
|
-
// DIRECT MODE: Run in single process without server
|
|
253
|
-
let unsub = null
|
|
254
|
-
|
|
255
|
-
try {
|
|
256
|
-
// Create a session directly
|
|
257
|
-
const session = await Session.createNext({
|
|
258
|
-
directory: process.cwd()
|
|
259
|
-
})
|
|
260
|
-
const sessionID = session.id
|
|
261
|
-
|
|
262
|
-
// Subscribe to all bus events to output them in OpenCode format
|
|
263
|
-
const eventPromise = new Promise((resolve) => {
|
|
264
|
-
unsub = Bus.subscribeAll((event) => {
|
|
265
|
-
// Output events in OpenCode JSON format
|
|
266
|
-
if (event.type === 'message.part.updated') {
|
|
267
|
-
const part = event.properties.part
|
|
268
|
-
if (part.sessionID !== sessionID) return
|
|
269
|
-
|
|
270
|
-
// Output different event types (pretty-printed for readability)
|
|
271
|
-
if (part.type === 'step-start') {
|
|
272
|
-
process.stdout.write(JSON.stringify({
|
|
273
|
-
type: 'step_start',
|
|
274
|
-
timestamp: Date.now(),
|
|
275
|
-
sessionID,
|
|
276
|
-
part
|
|
277
|
-
}, null, 2) + EOL)
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
if (part.type === 'step-finish') {
|
|
281
|
-
process.stdout.write(JSON.stringify({
|
|
282
|
-
type: 'step_finish',
|
|
283
|
-
timestamp: Date.now(),
|
|
284
|
-
sessionID,
|
|
285
|
-
part
|
|
286
|
-
}, null, 2) + EOL)
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
if (part.type === 'text' && part.time?.end) {
|
|
290
|
-
process.stdout.write(JSON.stringify({
|
|
291
|
-
type: 'text',
|
|
292
|
-
timestamp: Date.now(),
|
|
293
|
-
sessionID,
|
|
294
|
-
part
|
|
295
|
-
}, null, 2) + EOL)
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
if (part.type === 'tool' && part.state.status === 'completed') {
|
|
299
|
-
process.stdout.write(JSON.stringify({
|
|
300
|
-
type: 'tool_use',
|
|
301
|
-
timestamp: Date.now(),
|
|
302
|
-
sessionID,
|
|
303
|
-
part
|
|
304
|
-
}, null, 2) + EOL)
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// Handle session idle to know when to stop
|
|
309
|
-
if (event.type === 'session.idle' && event.properties.sessionID === sessionID) {
|
|
310
|
-
resolve()
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// Handle errors
|
|
314
|
-
if (event.type === 'session.error') {
|
|
315
|
-
const props = event.properties
|
|
316
|
-
if (props.sessionID !== sessionID || !props.error) return
|
|
317
|
-
process.stdout.write(JSON.stringify({
|
|
318
|
-
type: 'error',
|
|
319
|
-
timestamp: Date.now(),
|
|
320
|
-
sessionID,
|
|
321
|
-
error: props.error
|
|
322
|
-
}, null, 2) + EOL)
|
|
323
|
-
}
|
|
324
|
-
})
|
|
325
|
-
})
|
|
326
|
-
|
|
327
|
-
// Send message to session directly
|
|
328
|
-
const message = request.message || "hi"
|
|
329
|
-
const parts = [{ type: "text", text: message }]
|
|
552
|
+
.option('verbose', {
|
|
553
|
+
type: 'boolean',
|
|
554
|
+
description:
|
|
555
|
+
'Enable verbose mode to debug API requests (shows system prompt, token counts, etc.)',
|
|
556
|
+
default: false,
|
|
557
|
+
})
|
|
558
|
+
.option('use-existing-claude-oauth', {
|
|
559
|
+
type: 'boolean',
|
|
560
|
+
description:
|
|
561
|
+
'Use existing Claude OAuth credentials from ~/.claude/.credentials.json (from Claude Code CLI)',
|
|
562
|
+
default: false,
|
|
563
|
+
})
|
|
564
|
+
.help().argv;
|
|
330
565
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
parts,
|
|
335
|
-
model: {
|
|
336
|
-
providerID,
|
|
337
|
-
modelID
|
|
338
|
-
},
|
|
339
|
-
system: systemMessage,
|
|
340
|
-
appendSystem: appendSystemMessage
|
|
341
|
-
}).catch((error) => {
|
|
342
|
-
process.stdout.write(JSON.stringify({
|
|
343
|
-
type: 'error',
|
|
344
|
-
timestamp: Date.now(),
|
|
345
|
-
sessionID,
|
|
346
|
-
error: error instanceof Error ? error.message : String(error)
|
|
347
|
-
}, null, 2) + EOL)
|
|
348
|
-
})
|
|
566
|
+
// If a command was executed (like mcp), yargs handles it
|
|
567
|
+
// Otherwise, check if we should run in agent mode (stdin piped)
|
|
568
|
+
const commandExecuted = argv._ && argv._.length > 0;
|
|
349
569
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
// Always clean up resources
|
|
354
|
-
if (unsub) unsub()
|
|
355
|
-
await Instance.dispose()
|
|
356
|
-
}
|
|
570
|
+
if (!commandExecuted) {
|
|
571
|
+
// No command specified, run in default agent mode (stdin processing)
|
|
572
|
+
await runAgentMode(argv);
|
|
357
573
|
}
|
|
358
|
-
|
|
359
|
-
// Explicitly exit to ensure process terminates
|
|
360
|
-
process.exit(0)
|
|
361
574
|
} catch (error) {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
575
|
+
hasError = true;
|
|
576
|
+
console.error(
|
|
577
|
+
JSON.stringify(
|
|
578
|
+
{
|
|
579
|
+
type: 'error',
|
|
580
|
+
timestamp: Date.now(),
|
|
581
|
+
errorType: error instanceof Error ? error.name : 'Error',
|
|
582
|
+
message: error instanceof Error ? error.message : String(error),
|
|
583
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
584
|
+
},
|
|
585
|
+
null,
|
|
586
|
+
2
|
|
587
|
+
)
|
|
588
|
+
);
|
|
589
|
+
process.exit(1);
|
|
368
590
|
}
|
|
369
591
|
}
|
|
370
592
|
|
|
371
|
-
main()
|
|
593
|
+
main();
|