@link-assistant/agent 0.3.0 → 0.4.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 +24 -0
- package/package.json +1 -1
- package/src/auth/plugins.ts +2 -0
- package/src/bun/index.ts +105 -18
- package/src/cli/input-queue.js +197 -0
- package/src/index.js +166 -29
- package/src/provider/provider.ts +5 -1
- package/src/session/prompt.ts +21 -4
package/README.md
CHANGED
|
@@ -181,6 +181,18 @@ See [MODELS.md](MODELS.md) for complete list of available models and pricing.
|
|
|
181
181
|
See [docs/groq.md](docs/groq.md) for Groq provider documentation.
|
|
182
182
|
See [docs/claude-oauth.md](docs/claude-oauth.md) for Claude OAuth provider documentation.
|
|
183
183
|
|
|
184
|
+
### Direct Prompt Mode
|
|
185
|
+
|
|
186
|
+
Use `-p`/`--prompt` to send a prompt directly without reading from stdin:
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
# Direct prompt (bypasses stdin)
|
|
190
|
+
agent -p "What is 2+2?"
|
|
191
|
+
|
|
192
|
+
# Useful in scripts
|
|
193
|
+
result=$(agent -p "Summarize: $(cat file.txt)")
|
|
194
|
+
```
|
|
195
|
+
|
|
184
196
|
### CLI Options
|
|
185
197
|
|
|
186
198
|
```bash
|
|
@@ -197,6 +209,16 @@ Options:
|
|
|
197
209
|
--system-message-file Full override of the system message from file
|
|
198
210
|
--append-system-message Append to the default system message
|
|
199
211
|
--append-system-message-file Append to the default system message from file
|
|
212
|
+
|
|
213
|
+
Stdin Mode Options:
|
|
214
|
+
-p, --prompt Direct prompt (bypasses stdin reading)
|
|
215
|
+
--disable-stdin Disable stdin streaming (requires --prompt)
|
|
216
|
+
--stdin-stream-timeout Timeout in ms for stdin reading (default: none)
|
|
217
|
+
--interactive Accept plain text input (default: true)
|
|
218
|
+
--no-interactive Only accept JSON input
|
|
219
|
+
--auto-merge-queued-messages Merge rapidly arriving lines (default: true)
|
|
220
|
+
--no-auto-merge-queued-messages Treat each line as separate message
|
|
221
|
+
|
|
200
222
|
--help Show help
|
|
201
223
|
--version Show version number
|
|
202
224
|
|
|
@@ -208,6 +230,8 @@ Commands:
|
|
|
208
230
|
mcp MCP server commands
|
|
209
231
|
```
|
|
210
232
|
|
|
233
|
+
See [docs/stdin-mode.md](docs/stdin-mode.md) for comprehensive stdin mode documentation.
|
|
234
|
+
|
|
211
235
|
### JSON Output Standards
|
|
212
236
|
|
|
213
237
|
The agent supports two JSON output format standards via the `--json-standard` option:
|
package/package.json
CHANGED
package/src/auth/plugins.ts
CHANGED
|
@@ -849,6 +849,8 @@ const GOOGLE_OAUTH_SCOPES = [
|
|
|
849
849
|
'https://www.googleapis.com/auth/cloud-platform',
|
|
850
850
|
'https://www.googleapis.com/auth/userinfo.email',
|
|
851
851
|
'https://www.googleapis.com/auth/userinfo.profile',
|
|
852
|
+
'https://www.googleapis.com/auth/generative-language.tuning',
|
|
853
|
+
'https://www.googleapis.com/auth/generative-language.retriever',
|
|
852
854
|
];
|
|
853
855
|
|
|
854
856
|
// Google OAuth endpoints
|
package/src/bun/index.ts
CHANGED
|
@@ -5,10 +5,14 @@ import path from 'path';
|
|
|
5
5
|
import { NamedError } from '../util/error';
|
|
6
6
|
import { readableStreamToText } from 'bun';
|
|
7
7
|
import { Flag } from '../flag/flag';
|
|
8
|
+
import { Lock } from '../util/lock';
|
|
8
9
|
|
|
9
10
|
export namespace BunProc {
|
|
10
11
|
const log = Log.create({ service: 'bun' });
|
|
11
12
|
|
|
13
|
+
// Lock key for serializing package installations to prevent race conditions
|
|
14
|
+
const INSTALL_LOCK_KEY = 'bun-install';
|
|
15
|
+
|
|
12
16
|
export async function run(
|
|
13
17
|
cmd: string[],
|
|
14
18
|
options?: Bun.SpawnOptions.OptionsObject<any, any, any>
|
|
@@ -65,8 +69,38 @@ export namespace BunProc {
|
|
|
65
69
|
})
|
|
66
70
|
);
|
|
67
71
|
|
|
72
|
+
// Maximum number of retry attempts for cache-related errors
|
|
73
|
+
const MAX_RETRIES = 3;
|
|
74
|
+
// Delay between retries in milliseconds
|
|
75
|
+
const RETRY_DELAY_MS = 500;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Check if an error is related to Bun cache issues
|
|
79
|
+
*/
|
|
80
|
+
function isCacheRelatedError(errorMsg: string): boolean {
|
|
81
|
+
return (
|
|
82
|
+
errorMsg.includes('failed copying files from cache') ||
|
|
83
|
+
errorMsg.includes('FileNotFound') ||
|
|
84
|
+
errorMsg.includes('ENOENT') ||
|
|
85
|
+
errorMsg.includes('EACCES') ||
|
|
86
|
+
errorMsg.includes('EBUSY')
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Wait for a specified duration
|
|
92
|
+
*/
|
|
93
|
+
function delay(ms: number): Promise<void> {
|
|
94
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
95
|
+
}
|
|
96
|
+
|
|
68
97
|
export async function install(pkg: string, version = 'latest') {
|
|
69
98
|
const mod = path.join(Global.Path.cache, 'node_modules', pkg);
|
|
99
|
+
|
|
100
|
+
// Use a write lock to serialize all package installations
|
|
101
|
+
// This prevents race conditions when multiple packages are installed concurrently
|
|
102
|
+
using _ = await Lock.write(INSTALL_LOCK_KEY);
|
|
103
|
+
|
|
70
104
|
const pkgjson = Bun.file(path.join(Global.Path.cache, 'package.json'));
|
|
71
105
|
const parsed = await pkgjson.json().catch(async () => {
|
|
72
106
|
const result = { dependencies: {} };
|
|
@@ -108,25 +142,78 @@ export namespace BunProc {
|
|
|
108
142
|
version,
|
|
109
143
|
});
|
|
110
144
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
145
|
+
// Retry logic for cache-related errors
|
|
146
|
+
let lastError: Error | undefined;
|
|
147
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
148
|
+
try {
|
|
149
|
+
await BunProc.run(args, {
|
|
150
|
+
cwd: Global.Path.cache,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
log.info('package installed successfully', { pkg, version, attempt });
|
|
154
|
+
parsed.dependencies[pkg] = version;
|
|
155
|
+
await Bun.write(pkgjson.name!, JSON.stringify(parsed, null, 2));
|
|
156
|
+
return mod;
|
|
157
|
+
} catch (e) {
|
|
158
|
+
const errorMsg = e instanceof Error ? e.message : String(e);
|
|
159
|
+
const isCacheError = isCacheRelatedError(errorMsg);
|
|
160
|
+
|
|
161
|
+
log.warn('package installation attempt failed', {
|
|
162
|
+
pkg,
|
|
163
|
+
version,
|
|
164
|
+
attempt,
|
|
165
|
+
maxRetries: MAX_RETRIES,
|
|
166
|
+
error: errorMsg,
|
|
167
|
+
isCacheError,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (isCacheError && attempt < MAX_RETRIES) {
|
|
171
|
+
log.info('retrying installation after cache-related error', {
|
|
172
|
+
pkg,
|
|
173
|
+
version,
|
|
174
|
+
attempt,
|
|
175
|
+
nextAttempt: attempt + 1,
|
|
176
|
+
delayMs: RETRY_DELAY_MS,
|
|
177
|
+
});
|
|
178
|
+
await delay(RETRY_DELAY_MS);
|
|
179
|
+
lastError = e instanceof Error ? e : new Error(errorMsg);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Non-cache error or final attempt - log and throw
|
|
184
|
+
log.error('package installation failed', {
|
|
185
|
+
pkg,
|
|
186
|
+
version,
|
|
187
|
+
error: errorMsg,
|
|
188
|
+
stack: e instanceof Error ? e.stack : undefined,
|
|
189
|
+
possibleCacheCorruption: isCacheError,
|
|
190
|
+
attempts: attempt,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Provide helpful recovery instructions for cache-related errors
|
|
194
|
+
if (isCacheError) {
|
|
195
|
+
log.error(
|
|
196
|
+
'Bun package cache may be corrupted. Try clearing the cache with: bun pm cache rm'
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
throw new InstallFailedError(
|
|
201
|
+
{ pkg, version, details: errorMsg },
|
|
202
|
+
{
|
|
203
|
+
cause: e,
|
|
204
|
+
}
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// This should not be reached, but handle it just in case
|
|
210
|
+
throw new InstallFailedError(
|
|
211
|
+
{
|
|
115
212
|
pkg,
|
|
116
213
|
version,
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
{ pkg, version, details: e instanceof Error ? e.message : String(e) },
|
|
122
|
-
{
|
|
123
|
-
cause: e,
|
|
124
|
-
}
|
|
125
|
-
);
|
|
126
|
-
});
|
|
127
|
-
log.info('package installed successfully', { pkg, version });
|
|
128
|
-
parsed.dependencies[pkg] = version;
|
|
129
|
-
await Bun.write(pkgjson.name!, JSON.stringify(parsed, null, 2));
|
|
130
|
-
return mod;
|
|
214
|
+
details: lastError?.message ?? 'Installation failed after all retries',
|
|
215
|
+
},
|
|
216
|
+
{ cause: lastError }
|
|
217
|
+
);
|
|
131
218
|
}
|
|
132
219
|
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input queue for managing continuous stdin input.
|
|
3
|
+
* Supports queuing of multiple messages and auto-merging of rapidly arriving lines.
|
|
4
|
+
*/
|
|
5
|
+
export class InputQueue {
|
|
6
|
+
constructor(options = {}) {
|
|
7
|
+
this.queue = [];
|
|
8
|
+
this.autoMerge = options.autoMerge !== false; // enabled by default
|
|
9
|
+
this.mergeDelayMs = options.mergeDelayMs || 50; // delay to wait for more lines
|
|
10
|
+
this.pendingLines = [];
|
|
11
|
+
this.mergeTimer = null;
|
|
12
|
+
this.onMessage = options.onMessage || (() => {});
|
|
13
|
+
this.interactive = options.interactive !== false; // enabled by default
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Parse input and determine if it's JSON or plain text
|
|
18
|
+
* @param {string} input - Raw input string
|
|
19
|
+
* @returns {object} - Parsed message object
|
|
20
|
+
*/
|
|
21
|
+
parseInput(input) {
|
|
22
|
+
const trimmed = input.trim();
|
|
23
|
+
if (!trimmed) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(trimmed);
|
|
29
|
+
// If it has a message field, use it directly
|
|
30
|
+
if (typeof parsed === 'object' && parsed !== null) {
|
|
31
|
+
return parsed;
|
|
32
|
+
}
|
|
33
|
+
// Otherwise wrap it
|
|
34
|
+
return { message: JSON.stringify(parsed) };
|
|
35
|
+
} catch (_e) {
|
|
36
|
+
// Not JSON, treat as plain text message
|
|
37
|
+
return { message: trimmed };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Add a line to the queue, potentially merging with pending lines
|
|
43
|
+
* @param {string} line - Input line
|
|
44
|
+
*/
|
|
45
|
+
addLine(line) {
|
|
46
|
+
if (!this.interactive && !line.trim().startsWith('{')) {
|
|
47
|
+
// In non-interactive mode, only accept JSON
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (this.autoMerge) {
|
|
52
|
+
// Add to pending lines and schedule merge
|
|
53
|
+
this.pendingLines.push(line);
|
|
54
|
+
this.scheduleMerge();
|
|
55
|
+
} else {
|
|
56
|
+
// No merging, queue immediately
|
|
57
|
+
const parsed = this.parseInput(line);
|
|
58
|
+
if (parsed) {
|
|
59
|
+
this.queue.push(parsed);
|
|
60
|
+
this.notifyMessage(parsed);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Schedule a merge of pending lines after a short delay
|
|
67
|
+
*/
|
|
68
|
+
scheduleMerge() {
|
|
69
|
+
if (this.mergeTimer) {
|
|
70
|
+
clearTimeout(this.mergeTimer);
|
|
71
|
+
}
|
|
72
|
+
this.mergeTimer = setTimeout(() => {
|
|
73
|
+
this.flushPendingLines();
|
|
74
|
+
}, this.mergeDelayMs);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Flush pending lines into a single merged message
|
|
79
|
+
*/
|
|
80
|
+
flushPendingLines() {
|
|
81
|
+
if (this.pendingLines.length === 0) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const mergedText = this.pendingLines.join('\n');
|
|
86
|
+
this.pendingLines = [];
|
|
87
|
+
this.mergeTimer = null;
|
|
88
|
+
|
|
89
|
+
const parsed = this.parseInput(mergedText);
|
|
90
|
+
if (parsed) {
|
|
91
|
+
this.queue.push(parsed);
|
|
92
|
+
this.notifyMessage(parsed);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Immediately flush any pending lines (for shutdown)
|
|
98
|
+
*/
|
|
99
|
+
flush() {
|
|
100
|
+
if (this.mergeTimer) {
|
|
101
|
+
clearTimeout(this.mergeTimer);
|
|
102
|
+
this.mergeTimer = null;
|
|
103
|
+
}
|
|
104
|
+
this.flushPendingLines();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get next message from the queue
|
|
109
|
+
* @returns {object|null} - Next message or null if queue is empty
|
|
110
|
+
*/
|
|
111
|
+
dequeue() {
|
|
112
|
+
return this.queue.shift() || null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Check if queue has messages
|
|
117
|
+
* @returns {boolean}
|
|
118
|
+
*/
|
|
119
|
+
hasMessages() {
|
|
120
|
+
return this.queue.length > 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Notify listener of new message
|
|
125
|
+
* @param {object} message - The message object
|
|
126
|
+
*/
|
|
127
|
+
notifyMessage(message) {
|
|
128
|
+
if (this.onMessage) {
|
|
129
|
+
this.onMessage(message);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get queue size
|
|
135
|
+
* @returns {number}
|
|
136
|
+
*/
|
|
137
|
+
size() {
|
|
138
|
+
return this.queue.length;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Create a continuous stdin reader that queues input lines
|
|
144
|
+
* @param {object} options - Options for the reader
|
|
145
|
+
* @returns {object} - Reader with queue and control methods
|
|
146
|
+
*/
|
|
147
|
+
// Note: This function is prepared for future continuous input mode
|
|
148
|
+
export function createContinuousStdinReader(options = {}) {
|
|
149
|
+
const inputQueue = new InputQueue(options);
|
|
150
|
+
let isRunning = true;
|
|
151
|
+
let lineBuffer = '';
|
|
152
|
+
|
|
153
|
+
const handleData = (chunk) => {
|
|
154
|
+
if (!isRunning) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
lineBuffer += chunk.toString();
|
|
159
|
+
const lines = lineBuffer.split('\n');
|
|
160
|
+
|
|
161
|
+
// Keep the last incomplete line in buffer
|
|
162
|
+
lineBuffer = lines.pop() || '';
|
|
163
|
+
|
|
164
|
+
// Process complete lines
|
|
165
|
+
for (const line of lines) {
|
|
166
|
+
if (line.trim()) {
|
|
167
|
+
inputQueue.addLine(line);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const handleEnd = () => {
|
|
173
|
+
// Process any remaining data in buffer
|
|
174
|
+
if (lineBuffer.trim()) {
|
|
175
|
+
inputQueue.addLine(lineBuffer);
|
|
176
|
+
}
|
|
177
|
+
inputQueue.flush();
|
|
178
|
+
isRunning = false;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
process.stdin.on('data', handleData);
|
|
182
|
+
process.stdin.on('end', handleEnd);
|
|
183
|
+
process.stdin.on('error', () => {
|
|
184
|
+
isRunning = false;
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
queue: inputQueue,
|
|
189
|
+
stop: () => {
|
|
190
|
+
isRunning = false;
|
|
191
|
+
inputQueue.flush();
|
|
192
|
+
process.stdin.removeListener('data', handleData);
|
|
193
|
+
process.stdin.removeListener('end', handleEnd);
|
|
194
|
+
},
|
|
195
|
+
isRunning: () => isRunning,
|
|
196
|
+
};
|
|
197
|
+
}
|
package/src/index.js
CHANGED
|
@@ -73,32 +73,71 @@ process.on('unhandledRejection', (reason, _promise) => {
|
|
|
73
73
|
process.exit(1);
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Read stdin with optional timeout.
|
|
78
|
+
* @param {number|null} timeout - Timeout in milliseconds. If null, wait indefinitely until EOF.
|
|
79
|
+
* @returns {Promise<string>} - The stdin content
|
|
80
|
+
*/
|
|
81
|
+
function readStdinWithTimeout(timeout = null) {
|
|
82
|
+
return new Promise((resolve) => {
|
|
78
83
|
let data = '';
|
|
84
|
+
let hasData = false;
|
|
85
|
+
let timer = null;
|
|
86
|
+
|
|
87
|
+
const cleanup = () => {
|
|
88
|
+
if (timer) {
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
}
|
|
91
|
+
process.stdin.removeListener('data', onData);
|
|
92
|
+
process.stdin.removeListener('end', onEnd);
|
|
93
|
+
process.stdin.removeListener('error', onError);
|
|
94
|
+
};
|
|
95
|
+
|
|
79
96
|
const onData = (chunk) => {
|
|
97
|
+
hasData = true;
|
|
98
|
+
if (timer) {
|
|
99
|
+
clearTimeout(timer);
|
|
100
|
+
}
|
|
80
101
|
data += chunk;
|
|
81
102
|
};
|
|
103
|
+
|
|
82
104
|
const onEnd = () => {
|
|
83
105
|
cleanup();
|
|
84
106
|
resolve(data);
|
|
85
107
|
};
|
|
86
|
-
|
|
108
|
+
|
|
109
|
+
const onError = () => {
|
|
87
110
|
cleanup();
|
|
88
|
-
|
|
89
|
-
};
|
|
90
|
-
const cleanup = () => {
|
|
91
|
-
process.stdin.removeListener('data', onData);
|
|
92
|
-
process.stdin.removeListener('end', onEnd);
|
|
93
|
-
process.stdin.removeListener('error', onError);
|
|
111
|
+
resolve('');
|
|
94
112
|
};
|
|
113
|
+
|
|
114
|
+
// Only set timeout if specified (not null)
|
|
115
|
+
if (timeout !== null) {
|
|
116
|
+
timer = setTimeout(() => {
|
|
117
|
+
if (!hasData) {
|
|
118
|
+
process.stdin.pause();
|
|
119
|
+
cleanup();
|
|
120
|
+
resolve('');
|
|
121
|
+
}
|
|
122
|
+
}, timeout);
|
|
123
|
+
}
|
|
124
|
+
|
|
95
125
|
process.stdin.on('data', onData);
|
|
96
126
|
process.stdin.on('end', onEnd);
|
|
97
127
|
process.stdin.on('error', onError);
|
|
98
128
|
});
|
|
99
129
|
}
|
|
100
130
|
|
|
101
|
-
|
|
131
|
+
/**
|
|
132
|
+
* Output JSON status message to stderr
|
|
133
|
+
* This prevents the status message from interfering with JSON output parsing
|
|
134
|
+
* @param {object} status - Status object to output
|
|
135
|
+
*/
|
|
136
|
+
function outputStatus(status) {
|
|
137
|
+
console.error(JSON.stringify(status));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function runAgentMode(argv, request) {
|
|
102
141
|
// Note: verbose flag and logging are now initialized in middleware
|
|
103
142
|
// See main() function for the middleware that sets up Flag and Log.init()
|
|
104
143
|
|
|
@@ -207,21 +246,6 @@ async function runAgentMode(argv) {
|
|
|
207
246
|
|
|
208
247
|
// Logging is already initialized in middleware, no need to call Log.init() again
|
|
209
248
|
|
|
210
|
-
// Read input from stdin
|
|
211
|
-
const input = await readStdin();
|
|
212
|
-
const trimmedInput = input.trim();
|
|
213
|
-
|
|
214
|
-
// Try to parse as JSON, if it fails treat it as plain text message
|
|
215
|
-
let request;
|
|
216
|
-
try {
|
|
217
|
-
request = JSON.parse(trimmedInput);
|
|
218
|
-
} catch (_e) {
|
|
219
|
-
// Not JSON, treat as plain text message
|
|
220
|
-
request = {
|
|
221
|
-
message: trimmedInput,
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
|
|
225
249
|
// Wrap in Instance.provide for OpenCode infrastructure
|
|
226
250
|
await Instance.provide({
|
|
227
251
|
directory: process.cwd(),
|
|
@@ -533,7 +557,7 @@ async function runDirectMode(
|
|
|
533
557
|
async function main() {
|
|
534
558
|
try {
|
|
535
559
|
// Parse command line arguments with subcommands
|
|
536
|
-
const
|
|
560
|
+
const yargsInstance = yargs(hideBin(process.argv))
|
|
537
561
|
.scriptName('agent')
|
|
538
562
|
.usage('$0 [command] [options]')
|
|
539
563
|
.version(pkg.version)
|
|
@@ -593,6 +617,34 @@ async function main() {
|
|
|
593
617
|
'Use existing Claude OAuth credentials from ~/.claude/.credentials.json (from Claude Code CLI)',
|
|
594
618
|
default: false,
|
|
595
619
|
})
|
|
620
|
+
.option('prompt', {
|
|
621
|
+
alias: 'p',
|
|
622
|
+
type: 'string',
|
|
623
|
+
description: 'Prompt message to send directly (bypasses stdin reading)',
|
|
624
|
+
})
|
|
625
|
+
.option('disable-stdin', {
|
|
626
|
+
type: 'boolean',
|
|
627
|
+
description:
|
|
628
|
+
'Disable stdin streaming mode (requires --prompt or shows help)',
|
|
629
|
+
default: false,
|
|
630
|
+
})
|
|
631
|
+
.option('stdin-stream-timeout', {
|
|
632
|
+
type: 'number',
|
|
633
|
+
description:
|
|
634
|
+
'Optional timeout in milliseconds for stdin reading (default: no timeout)',
|
|
635
|
+
})
|
|
636
|
+
.option('auto-merge-queued-messages', {
|
|
637
|
+
type: 'boolean',
|
|
638
|
+
description:
|
|
639
|
+
'Enable auto-merging of rapidly arriving input lines into single messages (default: true)',
|
|
640
|
+
default: true,
|
|
641
|
+
})
|
|
642
|
+
.option('interactive', {
|
|
643
|
+
type: 'boolean',
|
|
644
|
+
description:
|
|
645
|
+
'Enable interactive mode to accept manual input as plain text strings (default: true). Use --no-interactive to only accept JSON input.',
|
|
646
|
+
default: true,
|
|
647
|
+
})
|
|
596
648
|
// Initialize logging early for all CLI commands
|
|
597
649
|
// This prevents debug output from appearing in CLI unless --verbose is used
|
|
598
650
|
.middleware(async (argv) => {
|
|
@@ -641,15 +693,100 @@ async function main() {
|
|
|
641
693
|
process.exit(1);
|
|
642
694
|
}
|
|
643
695
|
})
|
|
644
|
-
.help()
|
|
696
|
+
.help();
|
|
697
|
+
|
|
698
|
+
const argv = await yargsInstance.argv;
|
|
645
699
|
|
|
646
700
|
// If a command was executed (like mcp), yargs handles it
|
|
647
701
|
// Otherwise, check if we should run in agent mode (stdin piped)
|
|
648
702
|
const commandExecuted = argv._ && argv._.length > 0;
|
|
649
703
|
|
|
650
704
|
if (!commandExecuted) {
|
|
651
|
-
//
|
|
652
|
-
|
|
705
|
+
// Check if --prompt flag was provided
|
|
706
|
+
if (argv.prompt) {
|
|
707
|
+
// Direct prompt mode - bypass stdin entirely
|
|
708
|
+
const request = { message: argv.prompt };
|
|
709
|
+
await runAgentMode(argv, request);
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// Check if --disable-stdin was set without --prompt
|
|
714
|
+
if (argv['disable-stdin']) {
|
|
715
|
+
// Output a helpful message suggesting to use --prompt
|
|
716
|
+
outputStatus({
|
|
717
|
+
type: 'error',
|
|
718
|
+
message:
|
|
719
|
+
'No prompt provided. Use -p/--prompt to specify a message, or remove --disable-stdin to read from stdin.',
|
|
720
|
+
hint: 'Example: agent -p "Hello, how are you?"',
|
|
721
|
+
});
|
|
722
|
+
process.exit(1);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Check if stdin is a TTY (interactive terminal)
|
|
726
|
+
// If it is, show help instead of waiting for input
|
|
727
|
+
if (process.stdin.isTTY) {
|
|
728
|
+
yargsInstance.showHelp();
|
|
729
|
+
process.exit(0);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// stdin is piped - enter stdin listening mode
|
|
733
|
+
// Output status message to inform user what's happening
|
|
734
|
+
const isInteractive = argv.interactive !== false;
|
|
735
|
+
const autoMerge = argv['auto-merge-queued-messages'] !== false;
|
|
736
|
+
|
|
737
|
+
outputStatus({
|
|
738
|
+
type: 'status',
|
|
739
|
+
mode: 'stdin-stream',
|
|
740
|
+
message:
|
|
741
|
+
'Agent CLI in stdin listening mode. Accepts JSON and plain text input.',
|
|
742
|
+
hint: 'Press CTRL+C to exit. Use --help for options.',
|
|
743
|
+
acceptedFormats: isInteractive
|
|
744
|
+
? ['JSON object with "message" field', 'Plain text']
|
|
745
|
+
: ['JSON object with "message" field'],
|
|
746
|
+
options: {
|
|
747
|
+
interactive: isInteractive,
|
|
748
|
+
autoMergeQueuedMessages: autoMerge,
|
|
749
|
+
},
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
// Read stdin with optional timeout
|
|
753
|
+
const timeout = argv['stdin-stream-timeout'] ?? null;
|
|
754
|
+
const input = await readStdinWithTimeout(timeout);
|
|
755
|
+
const trimmedInput = input.trim();
|
|
756
|
+
|
|
757
|
+
if (trimmedInput === '') {
|
|
758
|
+
outputStatus({
|
|
759
|
+
type: 'status',
|
|
760
|
+
message: 'No input received. Exiting.',
|
|
761
|
+
});
|
|
762
|
+
yargsInstance.showHelp();
|
|
763
|
+
process.exit(0);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// Try to parse as JSON, if it fails treat it as plain text message
|
|
767
|
+
let request;
|
|
768
|
+
try {
|
|
769
|
+
request = JSON.parse(trimmedInput);
|
|
770
|
+
} catch (_e) {
|
|
771
|
+
// Not JSON
|
|
772
|
+
if (!isInteractive) {
|
|
773
|
+
// In non-interactive mode, only accept JSON
|
|
774
|
+
outputStatus({
|
|
775
|
+
type: 'error',
|
|
776
|
+
message:
|
|
777
|
+
'Invalid JSON input. In non-interactive mode (--no-interactive), only JSON input is accepted.',
|
|
778
|
+
hint: 'Use --interactive to accept plain text, or provide valid JSON: {"message": "your text"}',
|
|
779
|
+
});
|
|
780
|
+
process.exit(1);
|
|
781
|
+
}
|
|
782
|
+
// In interactive mode, treat as plain text message
|
|
783
|
+
request = {
|
|
784
|
+
message: trimmedInput,
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Run agent mode
|
|
789
|
+
await runAgentMode(argv, request);
|
|
653
790
|
}
|
|
654
791
|
} catch (error) {
|
|
655
792
|
hasError = true;
|
package/src/provider/provider.ts
CHANGED
|
@@ -905,6 +905,10 @@ export namespace Provider {
|
|
|
905
905
|
if (opencodeProvider) {
|
|
906
906
|
const [model] = sort(Object.values(opencodeProvider.info.models));
|
|
907
907
|
if (model) {
|
|
908
|
+
log.info('using opencode provider as default', {
|
|
909
|
+
provider: opencodeProvider.info.id,
|
|
910
|
+
model: model.id,
|
|
911
|
+
});
|
|
908
912
|
return {
|
|
909
913
|
providerID: opencodeProvider.info.id,
|
|
910
914
|
modelID: model.id,
|
|
@@ -912,7 +916,7 @@ export namespace Provider {
|
|
|
912
916
|
}
|
|
913
917
|
}
|
|
914
918
|
|
|
915
|
-
// Fall back to any available provider
|
|
919
|
+
// Fall back to any available provider if opencode is not available
|
|
916
920
|
const provider = providers.find(
|
|
917
921
|
(p) => !cfg.provider || Object.keys(cfg.provider).includes(p.info.id)
|
|
918
922
|
);
|
package/src/session/prompt.ts
CHANGED
|
@@ -290,10 +290,27 @@ export namespace SessionPrompt {
|
|
|
290
290
|
history: msgs,
|
|
291
291
|
});
|
|
292
292
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
293
|
+
let model;
|
|
294
|
+
try {
|
|
295
|
+
model = await Provider.getModel(
|
|
296
|
+
lastUser.model.providerID,
|
|
297
|
+
lastUser.model.modelID
|
|
298
|
+
);
|
|
299
|
+
} catch (error) {
|
|
300
|
+
log.warn(
|
|
301
|
+
'Failed to initialize specified model, falling back to default model',
|
|
302
|
+
{
|
|
303
|
+
providerID: lastUser.model.providerID,
|
|
304
|
+
modelID: lastUser.model.modelID,
|
|
305
|
+
error: error instanceof Error ? error.message : String(error),
|
|
306
|
+
}
|
|
307
|
+
);
|
|
308
|
+
const defaultModel = await Provider.defaultModel();
|
|
309
|
+
model = await Provider.getModel(
|
|
310
|
+
defaultModel.providerID,
|
|
311
|
+
defaultModel.modelID
|
|
312
|
+
);
|
|
313
|
+
}
|
|
297
314
|
const task = tasks.pop();
|
|
298
315
|
|
|
299
316
|
// pending subtask
|