@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.
Files changed (104) hide show
  1. package/EXAMPLES.md +80 -1
  2. package/MODELS.md +72 -24
  3. package/README.md +95 -2
  4. package/TOOLS.md +20 -0
  5. package/package.json +36 -2
  6. package/src/agent/agent.ts +68 -54
  7. package/src/auth/claude-oauth.ts +426 -0
  8. package/src/auth/index.ts +28 -26
  9. package/src/auth/plugins.ts +876 -0
  10. package/src/bun/index.ts +53 -43
  11. package/src/bus/global.ts +5 -5
  12. package/src/bus/index.ts +59 -53
  13. package/src/cli/bootstrap.js +12 -12
  14. package/src/cli/bootstrap.ts +6 -6
  15. package/src/cli/cmd/agent.ts +97 -92
  16. package/src/cli/cmd/auth.ts +468 -0
  17. package/src/cli/cmd/cmd.ts +2 -2
  18. package/src/cli/cmd/export.ts +41 -41
  19. package/src/cli/cmd/mcp.ts +210 -53
  20. package/src/cli/cmd/models.ts +30 -29
  21. package/src/cli/cmd/run.ts +269 -213
  22. package/src/cli/cmd/stats.ts +185 -146
  23. package/src/cli/error.ts +17 -13
  24. package/src/cli/ui.ts +78 -0
  25. package/src/command/index.ts +26 -26
  26. package/src/config/config.ts +528 -288
  27. package/src/config/markdown.ts +15 -15
  28. package/src/file/ripgrep.ts +201 -169
  29. package/src/file/time.ts +21 -18
  30. package/src/file/watcher.ts +51 -42
  31. package/src/file.ts +1 -1
  32. package/src/flag/flag.ts +26 -11
  33. package/src/format/formatter.ts +206 -162
  34. package/src/format/index.ts +61 -61
  35. package/src/global/index.ts +21 -21
  36. package/src/id/id.ts +47 -33
  37. package/src/index.js +554 -332
  38. package/src/json-standard/index.ts +173 -0
  39. package/src/mcp/index.ts +135 -128
  40. package/src/patch/index.ts +336 -267
  41. package/src/project/bootstrap.ts +15 -15
  42. package/src/project/instance.ts +43 -36
  43. package/src/project/project.ts +47 -47
  44. package/src/project/state.ts +37 -33
  45. package/src/provider/models-macro.ts +5 -5
  46. package/src/provider/models.ts +32 -32
  47. package/src/provider/opencode.js +19 -19
  48. package/src/provider/provider.ts +518 -277
  49. package/src/provider/transform.ts +143 -102
  50. package/src/server/project.ts +21 -21
  51. package/src/server/server.ts +111 -105
  52. package/src/session/agent.js +66 -60
  53. package/src/session/compaction.ts +136 -111
  54. package/src/session/index.ts +189 -156
  55. package/src/session/message-v2.ts +312 -268
  56. package/src/session/message.ts +73 -57
  57. package/src/session/processor.ts +180 -166
  58. package/src/session/prompt.ts +678 -533
  59. package/src/session/retry.ts +26 -23
  60. package/src/session/revert.ts +76 -62
  61. package/src/session/status.ts +26 -26
  62. package/src/session/summary.ts +97 -76
  63. package/src/session/system.ts +77 -63
  64. package/src/session/todo.ts +22 -16
  65. package/src/snapshot/index.ts +92 -76
  66. package/src/storage/storage.ts +157 -120
  67. package/src/tool/bash.ts +116 -106
  68. package/src/tool/batch.ts +73 -59
  69. package/src/tool/codesearch.ts +60 -53
  70. package/src/tool/edit.ts +319 -263
  71. package/src/tool/glob.ts +32 -28
  72. package/src/tool/grep.ts +72 -53
  73. package/src/tool/invalid.ts +7 -7
  74. package/src/tool/ls.ts +77 -64
  75. package/src/tool/multiedit.ts +30 -21
  76. package/src/tool/patch.ts +121 -94
  77. package/src/tool/read.ts +140 -122
  78. package/src/tool/registry.ts +38 -38
  79. package/src/tool/task.ts +93 -60
  80. package/src/tool/todo.ts +16 -16
  81. package/src/tool/tool.ts +45 -36
  82. package/src/tool/webfetch.ts +97 -74
  83. package/src/tool/websearch.ts +78 -64
  84. package/src/tool/write.ts +21 -15
  85. package/src/util/binary.ts +27 -19
  86. package/src/util/context.ts +8 -8
  87. package/src/util/defer.ts +7 -5
  88. package/src/util/error.ts +24 -19
  89. package/src/util/eventloop.ts +16 -10
  90. package/src/util/filesystem.ts +37 -33
  91. package/src/util/fn.ts +11 -8
  92. package/src/util/iife.ts +1 -1
  93. package/src/util/keybind.ts +44 -44
  94. package/src/util/lazy.ts +7 -7
  95. package/src/util/locale.ts +20 -16
  96. package/src/util/lock.ts +43 -38
  97. package/src/util/log.ts +95 -85
  98. package/src/util/queue.ts +8 -8
  99. package/src/util/rpc.ts +35 -23
  100. package/src/util/scrap.ts +4 -4
  101. package/src/util/signal.ts +5 -5
  102. package/src/util/timeout.ts +6 -6
  103. package/src/util/token.ts +2 -2
  104. 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
- import { EOL } from 'os'
10
- import yargs from 'yargs'
11
- import { hideBin } from 'yargs/helpers'
12
-
13
- async function readStdin() {
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
- const onError = err => {
24
- cleanup()
25
- reject(err)
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
- const cleanup = () => {
28
- process.stdin.removeListener('data', onData)
29
- process.stdin.removeListener('end', onEnd)
30
- process.stdin.removeListener('error', onError)
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
- process.stdin.on('data', onData)
33
- process.stdin.on('end', onEnd)
34
- process.stdin.on('error', onError)
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
- .help()
69
- .argv
70
-
71
- // Parse model argument
72
- const modelParts = argv.model.split('/')
73
- const providerID = modelParts[0] || 'opencode'
74
- const modelID = modelParts[1] || 'grok-code'
75
-
76
- // Read system message files
77
- let systemMessage = argv['system-message']
78
- let appendSystemMessage = argv['append-system-message']
79
-
80
- if (argv['system-message-file']) {
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
- // Start the prompt directly without HTTP
332
- SessionPrompt.prompt({
333
- sessionID,
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
- // Wait for session to become idle
351
- await eventPromise
352
- } finally {
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
- console.error(JSON.stringify({
363
- type: 'error',
364
- timestamp: Date.now(),
365
- error: error instanceof Error ? error.message : String(error)
366
- }))
367
- process.exit(1)
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();