@4onstudios/iris-agent 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -15
- package/dist/api/acp/acpServer.js +113 -30
- package/dist/api/acp/providerSetup.d.ts +3 -0
- package/dist/api/acp/providerSetup.js +134 -0
- package/dist/api/agent.js +1 -1
- package/dist/api/core/agent/tools/listDirectory.d.ts +2 -2
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +9 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -142,15 +142,52 @@ standard error. Each ACP process is bound to the workspace supplied at startup.
|
|
|
142
142
|
To switch workspaces, close the process and respawn `iris-agent` with the new
|
|
143
143
|
`--workspace` path.
|
|
144
144
|
|
|
145
|
+
When the current directory is the target workspace, `--workspace` is optional:
|
|
146
|
+
|
|
147
|
+
```sh
|
|
148
|
+
OPENROUTER_API_KEY=... npx @4onstudios/iris-agent@latest --acp
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
If the selected model's credentials are missing, Iris Agent stops before opening
|
|
152
|
+
the ACP connection and prints the required environment variable, a terminal
|
|
153
|
+
command, and a ready-to-paste VS Code ACP Client configuration. Configure API
|
|
154
|
+
keys in `acp.agents.<name>.env`; do not put them in the argument list:
|
|
155
|
+
|
|
156
|
+
```json
|
|
157
|
+
{
|
|
158
|
+
"acp.agents": {
|
|
159
|
+
"Iris Agent": {
|
|
160
|
+
"command": "npx",
|
|
161
|
+
"args": ["@4onstudios/iris-agent@latest", "--acp"],
|
|
162
|
+
"env": {
|
|
163
|
+
"OPENROUTER_API_KEY": "your-openrouter-api-key"
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Use `--modelId` to select a direct provider: `openai/gpt-4o` with
|
|
171
|
+
`OPENAI_API_KEY`, `anthropic/claude-sonnet-4-5` with `ANTHROPIC_API_KEY`,
|
|
172
|
+
`google/gemini-2.5-pro` with `GOOGLE_GENERATIVE_AI_API_KEY`, or
|
|
173
|
+
`huggingface/...` with `HF_TOKEN`. Local `ollama/<model>` sessions do not need
|
|
174
|
+
a cloud API key.
|
|
175
|
+
|
|
176
|
+
Iris Agent advertises ACP session loading and listing, so
|
|
177
|
+
[VS Code ACP Client](https://github.com/formulahendry/vscode-acp) can display
|
|
178
|
+
and restore persisted conversations for the current workspace. It also returns
|
|
179
|
+
the per-session model configuration option required by the extension's composer
|
|
180
|
+
controls.
|
|
181
|
+
|
|
145
182
|
## CLI Usage
|
|
146
183
|
|
|
147
184
|
```sh
|
|
148
|
-
iris-agent --workspace <path> [--acp | --chat] [--modelId <model>]
|
|
185
|
+
iris-agent [--workspace <path>] [--acp | --chat] [--modelId <model>]
|
|
149
186
|
```
|
|
150
187
|
|
|
151
188
|
**Options:**
|
|
152
189
|
|
|
153
|
-
- `--workspace` (
|
|
190
|
+
- `--workspace` (`-w`) - Path to the workspace/project root; defaults to the current working directory
|
|
154
191
|
- `--acp` (`-a`) - Start ACP protocol server (stdio-based)
|
|
155
192
|
- `--chat` (`-c`) - Start interactive chat mode
|
|
156
193
|
- `--modelId` - Model identifier used for chat/ACP sessions (default: `openrouter/openai/gpt-4o` or `MODEL_ID` / `OPENROUTER_MODEL` env vars)
|
|
@@ -169,6 +206,9 @@ npm run cli -- --workspace . --chat --modelId openrouter/anthropic/claude-3.7-so
|
|
|
169
206
|
# ACP server for IDE integration
|
|
170
207
|
npm run cli -- --workspace . --acp
|
|
171
208
|
|
|
209
|
+
# ACP server from the current workspace
|
|
210
|
+
npx @4onstudios/iris-agent@latest --acp
|
|
211
|
+
|
|
172
212
|
# ACP server with custom default model
|
|
173
213
|
npm run cli -- --workspace . --acp --modelId openrouter/openai/gpt-4o
|
|
174
214
|
|
|
@@ -666,14 +706,9 @@ The project logo is available at
|
|
|
666
706
|
community references. Keep the logo unchanged when using it as the project
|
|
667
707
|
mark.
|
|
668
708
|
|
|
669
|
-
## Package
|
|
709
|
+
## Package
|
|
670
710
|
|
|
671
|
-
The package is
|
|
672
|
-
|
|
673
|
-
```sh
|
|
674
|
-
npm login
|
|
675
|
-
npm publish
|
|
676
|
-
```
|
|
711
|
+
The published package is available on [npm](https://www.npmjs.com/package/@4onstudios/iris-agent).
|
|
677
712
|
|
|
678
713
|
Yarn users can install the published package with:
|
|
679
714
|
|
|
@@ -681,10 +716,4 @@ Yarn users can install the published package with:
|
|
|
681
716
|
yarn global add @4onstudios/iris-agent
|
|
682
717
|
```
|
|
683
718
|
|
|
684
|
-
Publishing requires access to the `@4onstudios` npm scope. The package is
|
|
685
|
-
configured with public access, but npm credentials and organization
|
|
686
|
-
permissions must be supplied by the publisher.
|
|
687
|
-
|
|
688
|
-
See [RELEASING.md](./RELEASING.md) for npm publication and versioning steps.
|
|
689
|
-
|
|
690
719
|
This project is released under the [MIT License](LICENSE).
|
|
@@ -9,7 +9,18 @@ import { executeCommand } from "../core/agent/tools/executeCommand.js";
|
|
|
9
9
|
import { getSlashCommandDescriptors, isSlashCommandsFeatureEnabled, } from "../helpers/slashCommands.js";
|
|
10
10
|
import { extractTokenUsageFromChunkPayload, mergeTokenUsage, } from "../helpers/tokenUsage.js";
|
|
11
11
|
const DEFAULT_MAX_STEPS = 50;
|
|
12
|
+
const MAX_CONCURRENT_SESSION_LOADS = 16;
|
|
12
13
|
const CHAT_SESSIONS_DIR = path.join(os.homedir(), ".iris", "chat-sessions");
|
|
14
|
+
const hasPersistedSessionCwd = (persisted) => typeof persisted?.cwd === "string" && persisted.cwd.length > 0;
|
|
15
|
+
const getPersistedSessionTitle = (persisted) => typeof persisted?.title === "string" ? persisted.title : undefined;
|
|
16
|
+
const getPersistedSessionUpdatedAt = (persisted) => {
|
|
17
|
+
if (typeof persisted.updatedAt !== "number" &&
|
|
18
|
+
typeof persisted.updatedAt !== "string") {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
const timestamp = new Date(persisted.updatedAt);
|
|
22
|
+
return Number.isNaN(timestamp.getTime()) ? undefined : timestamp.toISOString();
|
|
23
|
+
};
|
|
13
24
|
const isSafeChatSessionId = (sessionId) => /^[A-Za-z0-9._:-]+$/.test(sessionId);
|
|
14
25
|
const getChatSessionPath = (sessionId) => path.join(CHAT_SESSIONS_DIR, `${sessionId}.json`);
|
|
15
26
|
const loadPersistedChatSession = async (sessionId) => {
|
|
@@ -26,13 +37,33 @@ const loadPersistedChatSession = async (sessionId) => {
|
|
|
26
37
|
return undefined;
|
|
27
38
|
}
|
|
28
39
|
};
|
|
29
|
-
const
|
|
40
|
+
const loadPersistedChatSessions = async (sessionIds) => {
|
|
41
|
+
const loaded = new Array(sessionIds.length);
|
|
42
|
+
let nextIndex = 0;
|
|
43
|
+
const worker = async () => {
|
|
44
|
+
while (nextIndex < sessionIds.length) {
|
|
45
|
+
const index = nextIndex;
|
|
46
|
+
nextIndex += 1;
|
|
47
|
+
const sessionId = sessionIds[index];
|
|
48
|
+
if (sessionId) {
|
|
49
|
+
loaded[index] = await loadPersistedChatSession(sessionId);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
await Promise.all(Array.from({ length: Math.min(MAX_CONCURRENT_SESSION_LOADS, sessionIds.length) }, worker));
|
|
54
|
+
return loaded.flatMap((persisted, index) => {
|
|
55
|
+
const sessionId = sessionIds[index];
|
|
56
|
+
return persisted && sessionId ? [{ sessionId, persisted }] : [];
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
const savePersistedChatSession = async (sessionId, messages, session, existing) => {
|
|
30
60
|
if (!isSafeChatSessionId(sessionId))
|
|
31
61
|
return;
|
|
32
62
|
const timestamp = Date.now();
|
|
33
63
|
const payload = {
|
|
34
64
|
id: sessionId,
|
|
35
|
-
|
|
65
|
+
cwd: path.resolve(session.cwd),
|
|
66
|
+
title: session.title ?? getPersistedSessionTitle(existing),
|
|
36
67
|
createdAt: existing?.createdAt ?? timestamp,
|
|
37
68
|
updatedAt: timestamp,
|
|
38
69
|
messages,
|
|
@@ -92,6 +123,18 @@ export function assertAcpWorkspace(params, boundWorkspaceRoot) {
|
|
|
92
123
|
throw new Error(`This ACP process is bound to '${path.resolve(boundWorkspaceRoot)}'. Close it and respawn iris-agent with --workspace '${path.resolve(requestedWorkspace)}' to switch workspaces.`);
|
|
93
124
|
}
|
|
94
125
|
}
|
|
126
|
+
const resolveAcpWorkspace = (params, boundWorkspaceRoot) => {
|
|
127
|
+
if (boundWorkspaceRoot) {
|
|
128
|
+
assertAcpWorkspace(params, boundWorkspaceRoot);
|
|
129
|
+
return boundWorkspaceRoot;
|
|
130
|
+
}
|
|
131
|
+
const requestedWorkspace = typeof params?.workspaceRoot === "string"
|
|
132
|
+
? params.workspaceRoot
|
|
133
|
+
: typeof params?.cwd === "string"
|
|
134
|
+
? params.cwd
|
|
135
|
+
: undefined;
|
|
136
|
+
return path.resolve(requestedWorkspace || process.cwd());
|
|
137
|
+
};
|
|
95
138
|
const toPromptText = (prompt) => prompt
|
|
96
139
|
.map((block) => {
|
|
97
140
|
if (block.type === "text")
|
|
@@ -184,23 +227,34 @@ export const createAcpAgentApp = (agentProvider, workspaceRoot) => {
|
|
|
184
227
|
})
|
|
185
228
|
.catch(() => undefined);
|
|
186
229
|
};
|
|
230
|
+
const getConfigOptions = (modelId) => {
|
|
231
|
+
const currentModel = modelId || "default";
|
|
232
|
+
return [
|
|
233
|
+
{
|
|
234
|
+
id: "model",
|
|
235
|
+
name: "Model",
|
|
236
|
+
type: "select",
|
|
237
|
+
category: "model",
|
|
238
|
+
currentValue: currentModel,
|
|
239
|
+
options: [{ value: currentModel, name: currentModel }],
|
|
240
|
+
},
|
|
241
|
+
];
|
|
242
|
+
};
|
|
187
243
|
return acp
|
|
188
244
|
.agent({ name: "iris-agent" })
|
|
189
245
|
.onRequest(acp.methods.agent.initialize, async () => ({
|
|
190
246
|
protocolVersion: acp.PROTOCOL_VERSION,
|
|
191
247
|
agentCapabilities: {
|
|
192
248
|
loadSession: true,
|
|
193
|
-
sessionCapabilities: { close: {} },
|
|
249
|
+
sessionCapabilities: { close: {}, list: {} },
|
|
194
250
|
},
|
|
195
251
|
agentInfo: {
|
|
196
252
|
name: "iris-agent",
|
|
197
|
-
version: "0.
|
|
253
|
+
version: "0.2.0",
|
|
198
254
|
},
|
|
199
255
|
}))
|
|
200
256
|
.onRequest(acp.methods.agent.session.new, async (ctx) => {
|
|
201
|
-
|
|
202
|
-
assertAcpWorkspace({ cwd: ctx.params.cwd }, boundWorkspaceRoot);
|
|
203
|
-
}
|
|
257
|
+
const sessionWorkspace = resolveAcpWorkspace(ctx.params, boundWorkspaceRoot);
|
|
204
258
|
const irisMeta = readIrisMeta(ctx.params._meta);
|
|
205
259
|
const requestedModel = irisMeta.modelId ||
|
|
206
260
|
(typeof ctx.params.modelId === "string"
|
|
@@ -210,18 +264,19 @@ export const createAcpAgentApp = (agentProvider, workspaceRoot) => {
|
|
|
210
264
|
? irisMeta.chatSessionId
|
|
211
265
|
: randomUUID();
|
|
212
266
|
sessions.set(sessionId, {
|
|
213
|
-
cwd:
|
|
267
|
+
cwd: sessionWorkspace,
|
|
214
268
|
modelId: requestedModel,
|
|
215
269
|
history: [],
|
|
216
270
|
});
|
|
217
271
|
await notifyAvailableCommands(ctx, sessionId);
|
|
218
|
-
return {
|
|
272
|
+
return {
|
|
273
|
+
sessionId,
|
|
274
|
+
configOptions: getConfigOptions(requestedModel),
|
|
275
|
+
};
|
|
219
276
|
})
|
|
220
277
|
.onRequest(acp.methods.agent.session.load, async (ctx) => {
|
|
221
278
|
const sessionId = ctx.params.sessionId;
|
|
222
|
-
|
|
223
|
-
assertAcpWorkspace({ cwd: ctx.params.cwd }, boundWorkspaceRoot);
|
|
224
|
-
}
|
|
279
|
+
const requestedWorkspace = resolveAcpWorkspace(ctx.params, boundWorkspaceRoot);
|
|
225
280
|
const irisMeta = readIrisMeta(ctx.params._meta);
|
|
226
281
|
const requestedModel = irisMeta.modelId ||
|
|
227
282
|
(typeof ctx.params.modelId === "string"
|
|
@@ -231,10 +286,18 @@ export const createAcpAgentApp = (agentProvider, workspaceRoot) => {
|
|
|
231
286
|
if (!persisted) {
|
|
232
287
|
throw new Error(`No persisted chat session found for '${sessionId}'`);
|
|
233
288
|
}
|
|
289
|
+
const sessionWorkspace = hasPersistedSessionCwd(persisted)
|
|
290
|
+
? path.resolve(persisted.cwd)
|
|
291
|
+
: requestedWorkspace;
|
|
292
|
+
if (boundWorkspaceRoot &&
|
|
293
|
+
path.resolve(sessionWorkspace) !== path.resolve(boundWorkspaceRoot)) {
|
|
294
|
+
throw new Error(`Persisted ACP session '${sessionId}' belongs to '${sessionWorkspace}', not this process's bound workspace '${boundWorkspaceRoot}'.`);
|
|
295
|
+
}
|
|
234
296
|
const history = persisted.messages.slice();
|
|
235
297
|
sessions.set(sessionId, {
|
|
236
|
-
cwd:
|
|
298
|
+
cwd: sessionWorkspace,
|
|
237
299
|
modelId: requestedModel,
|
|
300
|
+
title: getPersistedSessionTitle(persisted),
|
|
238
301
|
history,
|
|
239
302
|
});
|
|
240
303
|
for (const message of history) {
|
|
@@ -247,7 +310,37 @@ export const createAcpAgentApp = (agentProvider, workspaceRoot) => {
|
|
|
247
310
|
});
|
|
248
311
|
}
|
|
249
312
|
await notifyAvailableCommands(ctx, sessionId);
|
|
250
|
-
return {};
|
|
313
|
+
return { configOptions: getConfigOptions(requestedModel) };
|
|
314
|
+
})
|
|
315
|
+
.onRequest(acp.methods.agent.session.list, async (ctx) => {
|
|
316
|
+
const requestedCwd = resolveAcpWorkspace(ctx.params, boundWorkspaceRoot);
|
|
317
|
+
let entries;
|
|
318
|
+
try {
|
|
319
|
+
entries = await fs.readdir(CHAT_SESSIONS_DIR, {
|
|
320
|
+
withFileTypes: true,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
if (error.code !== "ENOENT") {
|
|
325
|
+
throw error;
|
|
326
|
+
}
|
|
327
|
+
return { sessions: [] };
|
|
328
|
+
}
|
|
329
|
+
const persistedSessions = (await loadPersistedChatSessions(entries
|
|
330
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
|
|
331
|
+
.map((entry) => entry.name.slice(0, -".json".length))))
|
|
332
|
+
.filter(({ persisted }) => !hasPersistedSessionCwd(persisted) ||
|
|
333
|
+
path.resolve(persisted.cwd) === requestedCwd)
|
|
334
|
+
.map(({ sessionId, persisted }) => ({
|
|
335
|
+
sessionId,
|
|
336
|
+
cwd: hasPersistedSessionCwd(persisted)
|
|
337
|
+
? path.resolve(persisted.cwd)
|
|
338
|
+
: requestedCwd,
|
|
339
|
+
title: getPersistedSessionTitle(persisted),
|
|
340
|
+
updatedAt: getPersistedSessionUpdatedAt(persisted),
|
|
341
|
+
}));
|
|
342
|
+
persistedSessions.sort((left, right) => (right.updatedAt || "").localeCompare(left.updatedAt || ""));
|
|
343
|
+
return { sessions: persistedSessions };
|
|
251
344
|
})
|
|
252
345
|
.onRequest(acp.methods.agent.session.setConfigOption, async (ctx) => {
|
|
253
346
|
const sessionId = ctx.params.sessionId;
|
|
@@ -265,20 +358,7 @@ export const createAcpAgentApp = (agentProvider, workspaceRoot) => {
|
|
|
265
358
|
session.modelId = undefined;
|
|
266
359
|
}
|
|
267
360
|
}
|
|
268
|
-
|
|
269
|
-
const configOptions = [
|
|
270
|
-
{
|
|
271
|
-
id: "model",
|
|
272
|
-
name: "Model",
|
|
273
|
-
type: "select",
|
|
274
|
-
category: "model",
|
|
275
|
-
currentValue: currentModel,
|
|
276
|
-
options: [
|
|
277
|
-
{ value: currentModel, name: currentModel },
|
|
278
|
-
],
|
|
279
|
-
},
|
|
280
|
-
];
|
|
281
|
-
return { configOptions };
|
|
361
|
+
return { configOptions: getConfigOptions(session.modelId) };
|
|
282
362
|
})
|
|
283
363
|
.onRequest(acp.methods.agent.session.prompt, async (ctx) => {
|
|
284
364
|
const sessionId = ctx.params.sessionId;
|
|
@@ -305,10 +385,13 @@ export const createAcpAgentApp = (agentProvider, workspaceRoot) => {
|
|
|
305
385
|
session.history.push({ role: "user", content: promptText });
|
|
306
386
|
}
|
|
307
387
|
const persistTurn = async (assistantText) => {
|
|
388
|
+
if (promptText && !session.title) {
|
|
389
|
+
session.title = promptText.replace(/\s+/g, " ").slice(0, 120);
|
|
390
|
+
}
|
|
308
391
|
if (assistantText) {
|
|
309
392
|
session.history.push({ role: "assistant", content: assistantText });
|
|
310
393
|
}
|
|
311
|
-
await savePersistedChatSession(sessionId, session.history, await loadPersistedChatSession(sessionId));
|
|
394
|
+
await savePersistedChatSession(sessionId, session.history, session, await loadPersistedChatSession(sessionId));
|
|
312
395
|
};
|
|
313
396
|
const turnSignal = activeTurn.abortController.signal;
|
|
314
397
|
if (sessions.get(sessionId) !== session || turnSignal.aborted) {
|
|
@@ -809,6 +892,6 @@ export async function startAcpServer(runtimeAgentOrFactory, workspaceRoot) {
|
|
|
809
892
|
const output = Writable.toWeb(process.stdout);
|
|
810
893
|
const input = Readable.toWeb(process.stdin);
|
|
811
894
|
const connection = createAcpAgentApp(runtimeAgentOrFactory, workspaceRoot).connect(acp.ndJsonStream(output, input));
|
|
812
|
-
console.error("ACP agent ready: iris-agent@0.
|
|
895
|
+
console.error("ACP agent ready: iris-agent@0.2.0 (stdio)");
|
|
813
896
|
await connection.closed;
|
|
814
897
|
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
const getConfiguredKey = (environment, keys) => keys.some((key) => Boolean(environment[key]?.trim()));
|
|
2
|
+
const getRequiredProvider = (modelId, environment) => {
|
|
3
|
+
const normalizedModelId = modelId.endsWith("-other")
|
|
4
|
+
? modelId.slice(0, -"-other".length)
|
|
5
|
+
: modelId;
|
|
6
|
+
if (normalizedModelId.startsWith("ollama/") ||
|
|
7
|
+
normalizedModelId.startsWith("local/")) {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
if (normalizedModelId.startsWith("huggingface/") ||
|
|
11
|
+
normalizedModelId.endsWith(":fireworks-ai")) {
|
|
12
|
+
return getConfiguredKey(environment, ["HF_TOKEN"])
|
|
13
|
+
? undefined
|
|
14
|
+
: {
|
|
15
|
+
credential: "HF_TOKEN",
|
|
16
|
+
modelExample: "huggingface/Qwen/Qwen2.5-Coder-32B-Instruct",
|
|
17
|
+
description: "a Hugging Face Inference Providers token",
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
if (normalizedModelId.startsWith("openrouter/")) {
|
|
21
|
+
return getConfiguredKey(environment, ["OPENROUTER_API_KEY"])
|
|
22
|
+
? undefined
|
|
23
|
+
: {
|
|
24
|
+
credential: "OPENROUTER_API_KEY",
|
|
25
|
+
modelExample: "openrouter/openai/gpt-4o",
|
|
26
|
+
description: "an OpenRouter API key",
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
if (normalizedModelId.startsWith("openai/")) {
|
|
30
|
+
return getConfiguredKey(environment, ["OPENAI_API_KEY"])
|
|
31
|
+
? undefined
|
|
32
|
+
: {
|
|
33
|
+
credential: "OPENAI_API_KEY",
|
|
34
|
+
modelExample: "openai/gpt-4o",
|
|
35
|
+
description: "an OpenAI API key",
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (normalizedModelId.startsWith("anthropic/")) {
|
|
39
|
+
return getConfiguredKey(environment, ["ANTHROPIC_API_KEY"])
|
|
40
|
+
? undefined
|
|
41
|
+
: {
|
|
42
|
+
credential: "ANTHROPIC_API_KEY",
|
|
43
|
+
modelExample: "anthropic/claude-sonnet-4-5",
|
|
44
|
+
description: "an Anthropic API key",
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (normalizedModelId.startsWith("google/") ||
|
|
48
|
+
normalizedModelId.startsWith("gemini")) {
|
|
49
|
+
if (getConfiguredKey(environment, [
|
|
50
|
+
"GOOGLE_GENERATIVE_AI_API_KEY",
|
|
51
|
+
"GEMINI_API_KEY",
|
|
52
|
+
])) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
credential: "GOOGLE_GENERATIVE_AI_API_KEY",
|
|
57
|
+
modelExample: "google/gemini-2.5-pro",
|
|
58
|
+
description: "a Google AI API key",
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (normalizedModelId.includes("/")) {
|
|
62
|
+
return getConfiguredKey(environment, ["OPENROUTER_API_KEY"])
|
|
63
|
+
? undefined
|
|
64
|
+
: {
|
|
65
|
+
credential: "OPENROUTER_API_KEY",
|
|
66
|
+
modelExample: normalizedModelId,
|
|
67
|
+
description: "an OpenRouter API key",
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (normalizedModelId.startsWith("claude")) {
|
|
71
|
+
if (getConfiguredKey(environment, [
|
|
72
|
+
"ANTHROPIC_API_KEY",
|
|
73
|
+
"OPENROUTER_API_KEY",
|
|
74
|
+
])) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
credential: "ANTHROPIC_API_KEY",
|
|
79
|
+
modelExample: "anthropic/claude-sonnet-4-5",
|
|
80
|
+
description: "an Anthropic API key",
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
if (normalizedModelId.startsWith("gpt") ||
|
|
84
|
+
normalizedModelId.startsWith("o1") ||
|
|
85
|
+
normalizedModelId.startsWith("o3")) {
|
|
86
|
+
if (getConfiguredKey(environment, ["OPENAI_API_KEY", "OPENROUTER_API_KEY"])) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
credential: "OPENAI_API_KEY",
|
|
91
|
+
modelExample: "openai/gpt-4o",
|
|
92
|
+
description: "an OpenAI API key",
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return getConfiguredKey(environment, ["OPENAI_API_KEY", "OPENROUTER_API_KEY"])
|
|
96
|
+
? undefined
|
|
97
|
+
: {
|
|
98
|
+
credential: "OPENROUTER_API_KEY",
|
|
99
|
+
modelExample: `openrouter/${normalizedModelId}`,
|
|
100
|
+
description: "an OpenRouter API key",
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
export const getMissingProviderSetup = (modelId, environment = process.env) => {
|
|
104
|
+
const requiredProvider = getRequiredProvider(modelId, environment);
|
|
105
|
+
if (!requiredProvider)
|
|
106
|
+
return undefined;
|
|
107
|
+
const { credential, description, modelExample } = requiredProvider;
|
|
108
|
+
return [
|
|
109
|
+
`Iris Agent cannot start model '${modelId}' because ${credential} is not configured.`,
|
|
110
|
+
"",
|
|
111
|
+
`Provide ${description} before starting the agent:`,
|
|
112
|
+
` ${credential}=<your-api-key> npx @4onstudios/iris-agent@latest --acp --modelId ${modelId}`,
|
|
113
|
+
"",
|
|
114
|
+
"For VS Code ACP Client, add the key to the agent's environment in settings.json:",
|
|
115
|
+
JSON.stringify({
|
|
116
|
+
"acp.agents": {
|
|
117
|
+
"Iris Agent": {
|
|
118
|
+
command: "npx",
|
|
119
|
+
args: ["@4onstudios/iris-agent@latest", "--acp", "--modelId", modelId],
|
|
120
|
+
env: { [credential]: "<your-api-key>" },
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
}, null, 2),
|
|
124
|
+
"",
|
|
125
|
+
"Supported provider options:",
|
|
126
|
+
" OpenRouter: OPENROUTER_API_KEY with --modelId openrouter/openai/gpt-4o",
|
|
127
|
+
" OpenAI: OPENAI_API_KEY with --modelId openai/gpt-4o",
|
|
128
|
+
" Anthropic: ANTHROPIC_API_KEY with --modelId anthropic/claude-sonnet-4-5",
|
|
129
|
+
" Google: GOOGLE_GENERATIVE_AI_API_KEY with --modelId google/gemini-2.5-pro",
|
|
130
|
+
" Hugging Face: HF_TOKEN with --modelId huggingface/Qwen/Qwen2.5-Coder-32B-Instruct",
|
|
131
|
+
" Local Ollama: no cloud key; use --modelId ollama/<model> (optionally set OLLAMA_BASE_URL).",
|
|
132
|
+
`Example selected-model identifier: ${modelExample}`,
|
|
133
|
+
].join("\n");
|
|
134
|
+
};
|
package/dist/api/agent.js
CHANGED
|
@@ -465,7 +465,7 @@ const createGeneratedAgentRuntimeAdapter = (agent) => {
|
|
|
465
465
|
descriptor: {
|
|
466
466
|
id: "generated-agent-runtime",
|
|
467
467
|
name: "Generated Agent Runtime",
|
|
468
|
-
version: "0.
|
|
468
|
+
version: "0.2.0",
|
|
469
469
|
source: "external",
|
|
470
470
|
},
|
|
471
471
|
async startSession(context) {
|
|
@@ -51,13 +51,13 @@ export declare const listDirectoryTool: {
|
|
|
51
51
|
includeHidden: z.ZodDefault<z.ZodBoolean>;
|
|
52
52
|
maxDepth: z.ZodDefault<z.ZodNumber>;
|
|
53
53
|
}, "strip", z.ZodTypeAny, {
|
|
54
|
-
maxDepth?: number;
|
|
55
54
|
recursive?: boolean;
|
|
55
|
+
maxDepth?: number;
|
|
56
56
|
dirPath?: string;
|
|
57
57
|
includeHidden?: boolean;
|
|
58
58
|
}, {
|
|
59
|
-
maxDepth?: number;
|
|
60
59
|
recursive?: boolean;
|
|
60
|
+
maxDepth?: number;
|
|
61
61
|
dirPath?: string;
|
|
62
62
|
includeHidden?: boolean;
|
|
63
63
|
}>;
|
package/dist/cli.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* iris-agent CLI with ACP (Agent Client Protocol) support
|
|
4
4
|
* Usage:
|
|
5
|
-
* iris-agent --
|
|
5
|
+
* iris-agent --acp
|
|
6
6
|
* iris-agent --workspace /path/to/workspace --modelId openrouter/openai/gpt-4o --chat
|
|
7
7
|
*/
|
|
8
8
|
export {};
|
package/dist/cli.js
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* iris-agent CLI with ACP (Agent Client Protocol) support
|
|
4
4
|
* Usage:
|
|
5
|
-
* iris-agent --
|
|
5
|
+
* iris-agent --acp
|
|
6
6
|
* iris-agent --workspace /path/to/workspace --modelId openrouter/openai/gpt-4o --chat
|
|
7
7
|
*/
|
|
8
8
|
import yargs from "yargs";
|
|
9
9
|
import { hideBin } from "yargs/helpers";
|
|
10
10
|
import { createCodingAgent } from "./api/core/agent/index.js";
|
|
11
11
|
import { startAcpServer } from "./api/acp/acpServer.js";
|
|
12
|
+
import { getMissingProviderSetup } from "./api/acp/providerSetup.js";
|
|
12
13
|
const defaultModelId = process.env.MODEL_ID ||
|
|
13
14
|
process.env.OPENROUTER_MODEL ||
|
|
14
15
|
"openrouter/openai/gpt-4o";
|
|
@@ -17,7 +18,7 @@ const argv = yargs(hideBin(process.argv))
|
|
|
17
18
|
alias: "w",
|
|
18
19
|
type: "string",
|
|
19
20
|
description: "Workspace root path",
|
|
20
|
-
|
|
21
|
+
default: process.cwd(),
|
|
21
22
|
})
|
|
22
23
|
.option("acp", {
|
|
23
24
|
alias: "a",
|
|
@@ -58,6 +59,12 @@ async function main() {
|
|
|
58
59
|
console.log(`📁 Workspace: ${workspaceRoot}`);
|
|
59
60
|
console.log(`🤖 Model: ${modelId}`);
|
|
60
61
|
if (argv.acp) {
|
|
62
|
+
const missingProviderSetup = getMissingProviderSetup(modelId);
|
|
63
|
+
if (missingProviderSetup) {
|
|
64
|
+
console.error(missingProviderSetup);
|
|
65
|
+
process.exitCode = 1;
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
61
68
|
console.log("🔗 Starting ACP server over stdio...");
|
|
62
69
|
await startAcpServer((requestedModelId, targetWorkspace) => createCodingAgent(requestedModelId || modelId, targetWorkspace || workspaceRoot), workspaceRoot);
|
|
63
70
|
}
|