@edukors/ai-cli 0.0.0-stage → 0.1.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.
Files changed (106) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +162 -2
  3. package/dist/cli/main.d.ts +2 -0
  4. package/dist/cli/main.js +256 -0
  5. package/dist/cli/main.js.map +1 -0
  6. package/dist/constants.d.ts +2 -0
  7. package/dist/constants.js +3 -0
  8. package/dist/constants.js.map +1 -0
  9. package/dist/diagnostics.d.ts +28 -0
  10. package/dist/diagnostics.js +35 -0
  11. package/dist/diagnostics.js.map +1 -0
  12. package/dist/engine.d.ts +157 -0
  13. package/dist/engine.js +395 -0
  14. package/dist/engine.js.map +1 -0
  15. package/dist/errors.d.ts +27 -0
  16. package/dist/errors.js +59 -0
  17. package/dist/errors.js.map +1 -0
  18. package/dist/events.d.ts +95 -0
  19. package/dist/events.js +8 -0
  20. package/dist/events.js.map +1 -0
  21. package/dist/host/main.d.ts +2 -0
  22. package/dist/host/main.js +30 -0
  23. package/dist/host/main.js.map +1 -0
  24. package/dist/host/stdio.d.ts +34 -0
  25. package/dist/host/stdio.js +254 -0
  26. package/dist/host/stdio.js.map +1 -0
  27. package/dist/index.d.ts +34 -0
  28. package/dist/index.js +21 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/mcp/config.d.ts +19 -0
  31. package/dist/mcp/config.js +63 -0
  32. package/dist/mcp/config.js.map +1 -0
  33. package/dist/mcp/external.d.ts +55 -0
  34. package/dist/mcp/external.js +129 -0
  35. package/dist/mcp/external.js.map +1 -0
  36. package/dist/mcp/toolServer.d.ts +30 -0
  37. package/dist/mcp/toolServer.js +102 -0
  38. package/dist/mcp/toolServer.js.map +1 -0
  39. package/dist/platform/discovery.d.ts +23 -0
  40. package/dist/platform/discovery.js +64 -0
  41. package/dist/platform/discovery.js.map +1 -0
  42. package/dist/platform/env.d.ts +12 -0
  43. package/dist/platform/env.js +104 -0
  44. package/dist/platform/env.js.map +1 -0
  45. package/dist/platform/login.d.ts +16 -0
  46. package/dist/platform/login.js +42 -0
  47. package/dist/platform/login.js.map +1 -0
  48. package/dist/platform/paths.d.ts +19 -0
  49. package/dist/platform/paths.js +46 -0
  50. package/dist/platform/paths.js.map +1 -0
  51. package/dist/platform/process.d.ts +55 -0
  52. package/dist/platform/process.js +186 -0
  53. package/dist/platform/process.js.map +1 -0
  54. package/dist/provider.d.ts +156 -0
  55. package/dist/provider.js +2 -0
  56. package/dist/provider.js.map +1 -0
  57. package/dist/providers/claude/adapter.d.ts +43 -0
  58. package/dist/providers/claude/adapter.js +398 -0
  59. package/dist/providers/claude/adapter.js.map +1 -0
  60. package/dist/providers/claude/events.d.ts +36 -0
  61. package/dist/providers/claude/events.js +90 -0
  62. package/dist/providers/claude/events.js.map +1 -0
  63. package/dist/providers/codex/adapter.d.ts +28 -0
  64. package/dist/providers/codex/adapter.js +497 -0
  65. package/dist/providers/codex/adapter.js.map +1 -0
  66. package/dist/providers/codex/rpc.d.ts +17 -0
  67. package/dist/providers/codex/rpc.js +75 -0
  68. package/dist/providers/codex/rpc.js.map +1 -0
  69. package/dist/providers.d.ts +9 -0
  70. package/dist/providers.js +15 -0
  71. package/dist/providers.js.map +1 -0
  72. package/dist/redact.d.ts +7 -0
  73. package/dist/redact.js +48 -0
  74. package/dist/redact.js.map +1 -0
  75. package/dist/registry/lock.d.ts +18 -0
  76. package/dist/registry/lock.js +83 -0
  77. package/dist/registry/lock.js.map +1 -0
  78. package/dist/registry/registry.d.ts +38 -0
  79. package/dist/registry/registry.js +119 -0
  80. package/dist/registry/registry.js.map +1 -0
  81. package/dist/session.d.ts +126 -0
  82. package/dist/session.js +333 -0
  83. package/dist/session.js.map +1 -0
  84. package/dist/settings.d.ts +8 -0
  85. package/dist/settings.js +7 -0
  86. package/dist/settings.js.map +1 -0
  87. package/dist/skills.d.ts +24 -0
  88. package/dist/skills.js +91 -0
  89. package/dist/skills.js.map +1 -0
  90. package/dist/testing/fakeProvider.d.ts +22 -0
  91. package/dist/testing/fakeProvider.js +266 -0
  92. package/dist/testing/fakeProvider.js.map +1 -0
  93. package/dist/testing/index.d.ts +2 -0
  94. package/dist/testing/index.js +2 -0
  95. package/dist/testing/index.js.map +1 -0
  96. package/dist/tools.d.ts +45 -0
  97. package/dist/tools.js +32 -0
  98. package/dist/tools.js.map +1 -0
  99. package/dist/version.d.ts +2 -0
  100. package/dist/version.js +3 -0
  101. package/dist/version.js.map +1 -0
  102. package/fixtures/fake-ai.mjs +230 -0
  103. package/fixtures/fake-claude.mjs +62 -0
  104. package/fixtures/fake-codex.mjs +281 -0
  105. package/fixtures/fake-mcp-server.mjs +14 -0
  106. package/package.json +74 -4
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+ // A stand-in for the `claude` executable, for what the adapter runs directly:
3
+ // `--version` and `auth status|login|logout`, with the sign-in kept in
4
+ // CLAUDE_CONFIG_DIR as the real one keys it. Conversations go through the
5
+ // Agent SDK, which the tests replace (test/fakeClaudeSdk.ts).
6
+ //
7
+ // `auth login` prints what Claude Code 2.1.312 printed in the live checks: an
8
+ // address for when the browser does not open, then a prompt for a pasted code.
9
+ // FAKE_CLAUDE_LOGIN: unset = the browser finishes on its own; `code` = waits for
10
+ // a pasted code (`good-code` succeeds); `fail` = refused.
11
+ // Other toggle: FAKE_CLAUDE_VERSION.
12
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
13
+ import { basename, join } from "node:path";
14
+ import { createInterface } from "node:readline";
15
+
16
+ const args = process.argv.slice(2);
17
+ if (args[0] === "--version") {
18
+ console.log(`${process.env.FAKE_CLAUDE_VERSION ?? "2.1.312"} (Claude Code)`);
19
+ process.exit(0);
20
+ }
21
+ const dir = process.env.CLAUDE_CONFIG_DIR;
22
+ if (!dir) {
23
+ console.error("CLAUDE_CONFIG_DIR is not set");
24
+ process.exit(2);
25
+ }
26
+ const authFile = join(dir, "fake-auth.json");
27
+
28
+ if (args[0] === "auth" && args[1] === "status") {
29
+ const signed = existsSync(authFile) ? JSON.parse(readFileSync(authFile, "utf8")) : null;
30
+ console.log(JSON.stringify(signed ? { loggedIn: true, authMethod: "claude.ai", apiProvider: "firstParty", email: signed.email, configDirectory: dir } : { loggedIn: false, authMethod: "none", apiProvider: "firstParty", configDirectory: dir }, null, 2));
31
+ process.exit(signed ? 0 : 1);
32
+ }
33
+ if (args[0] === "auth" && args[1] === "login") {
34
+ const mode = process.env.FAKE_CLAUDE_LOGIN;
35
+ if (mode === "fail") {
36
+ console.error("OAuth error: access denied");
37
+ process.exit(1);
38
+ }
39
+ const succeed = () => {
40
+ mkdirSync(dir, { recursive: true });
41
+ writeFileSync(authFile, JSON.stringify({ email: `${basename(dir)}@example.org`, token: "fake-claude-oauth-token" }));
42
+ console.log("Login successful.");
43
+ process.exit(0);
44
+ };
45
+ console.log("Opening browser to sign in…");
46
+ console.log("If the browser didn't open, visit: https://claude.com/cai/oauth/authorize?code=true&client_id=fake-client&response_type=code&state=fake-state");
47
+ process.stdout.write("Paste code here if prompted > ");
48
+ if (mode !== "code") setTimeout(succeed, 100);
49
+ else
50
+ createInterface({ input: process.stdin }).once("line", (line) => {
51
+ if (line.trim() === "good-code") succeed();
52
+ console.log("OAuth error: Invalid code");
53
+ process.exit(1);
54
+ });
55
+ } else if (args[0] === "auth" && args[1] === "logout") {
56
+ rmSync(authFile, { force: true });
57
+ console.log("Successfully logged out.");
58
+ process.exit(0);
59
+ } else {
60
+ console.error(`unsupported: ${args.join(" ")}`);
61
+ process.exit(2);
62
+ }
@@ -0,0 +1,281 @@
1
+ #!/usr/bin/env node
2
+ // A stand-in for OpenAI's Codex CLI, for the tests. `codex app-server` speaks
3
+ // JSON-RPC over stdio without the "jsonrpc" field, keeps its sign-in and
4
+ // threads in CODEX_HOME, and reaches the application's tools through the MCP
5
+ // server given with `-c mcp_servers.<name>.url=...`, as the real one does.
6
+ //
7
+ // A turn depends on words in the request:
8
+ // tool <name> [<json>] calls an application tool over MCP
9
+ // mcp <server> <tool> [<json>] calls a tool of one of the user's servers, through the bridge
10
+ // skill <name> [<file>] reads a skill through read_skill
11
+ // developer reports the developer instructions it was given
12
+ // ask asks the user as Codex 0.164 does: an async message
13
+ // with questions, then sleeps until turn/steer answers
14
+ // ask-blocking asks the user through item/tool/requestUserInput
15
+ // comando asks to run a command and reports the decision
16
+ // slow streams until interrupted
17
+ // late after an interruption, still sends stale text
18
+ // crash exits in the middle of the turn
19
+ // limit fails with usageLimitExceeded
20
+ // env reports what it inherited
21
+ // anything else echoes the request
22
+ //
23
+ // Toggles: FAKE_CODEX_VERSION, FAKE_CODEX_LOGIN=fail.
24
+ import { createInterface } from "node:readline";
25
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
26
+ import { basename, join } from "node:path";
27
+ import { randomUUID } from "node:crypto";
28
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
29
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
30
+
31
+ const args = process.argv.slice(2);
32
+ if (args.includes("--version")) {
33
+ console.log(`codex-cli ${process.env.FAKE_CODEX_VERSION ?? "0.164.0"}`);
34
+ process.exit(0);
35
+ }
36
+ const HOME = process.env.CODEX_HOME;
37
+ if (!HOME) {
38
+ console.error("CODEX_HOME is not set");
39
+ process.exit(2);
40
+ }
41
+ mkdirSync(HOME, { recursive: true });
42
+ const authFile = join(HOME, "auth.json");
43
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
44
+
45
+ if (args[0] === "login" && args[1] === "status") {
46
+ if (existsSync(authFile)) {
47
+ console.log("Logged in using ChatGPT");
48
+ process.exit(0);
49
+ }
50
+ console.log("Not logged in");
51
+ process.exit(1);
52
+ }
53
+ if (args[0] === "login") {
54
+ if (process.env.FAKE_CODEX_LOGIN === "fail") {
55
+ console.error("login cancelled");
56
+ process.exit(1);
57
+ }
58
+ console.log("Starting local login server on http://localhost:1455.");
59
+ console.log("If your browser did not open, navigate to this URL to authenticate:\n");
60
+ console.log("https://auth.openai.com/oauth/authorize?response_type=code&client_id=fake-client&state=fake-state\n");
61
+ writeFileSync(authFile, JSON.stringify({ tokens: { access_token: `fake-codex-token-${randomUUID()}` }, email: basename(HOME) }));
62
+ console.log("Successfully logged in");
63
+ process.exit(0);
64
+ }
65
+ if (args[0] === "logout") {
66
+ rmSync(authFile, { force: true });
67
+ process.exit(0);
68
+ }
69
+ if (args[0] !== "app-server") {
70
+ console.error(`unknown command: ${args.join(" ")}`);
71
+ process.exit(2);
72
+ }
73
+
74
+ // -c key=value overrides, values in TOML (strings are JSON-compatible here).
75
+ const config = {};
76
+ for (let i = 0; i < args.length; i++)
77
+ if (args[i] === "-c") {
78
+ const [key, ...rest] = args[++i].split("=");
79
+ config[key] = JSON.parse(rest.join("="));
80
+ }
81
+ const mcpUrl = config["mcp_servers.edukors.url"];
82
+
83
+ const out = (msg) => process.stdout.write(JSON.stringify(msg) + "\n");
84
+ const notify = (method, params) => out({ method, params });
85
+ let nextId = 1000;
86
+ const waiting = new Map();
87
+ const askClient = (method, params) =>
88
+ new Promise((resolve) => {
89
+ const id = nextId++;
90
+ waiting.set(id, resolve);
91
+ out({ id, method, params });
92
+ });
93
+
94
+ const clients = new Map();
95
+ async function callTool(name, toolArgs, server = "edukors") {
96
+ if (!clients.has(server)) {
97
+ const url = config[`mcp_servers.${server}.url`];
98
+ const token = process.env[config[`mcp_servers.${server}.bearer_token_env_var`] ?? ""];
99
+ if (!url) return { text: `no MCP server ${server}`, isError: true };
100
+ const client = new Client({ name: "fake-codex", version: "0.164.0" });
101
+ await client.connect(new StreamableHTTPClientTransport(new URL(url), { requestInit: { headers: { Authorization: `Bearer ${token}` } } }));
102
+ clients.set(server, client);
103
+ }
104
+ const result = await clients.get(server).callTool({ name, arguments: toolArgs });
105
+ return { text: (result.content ?? []).map((c) => c.text ?? "").join("\n"), isError: Boolean(result.isError) };
106
+ }
107
+
108
+ let extraRoots = [];
109
+ const switchesFile = join(HOME, "skill-switches.json");
110
+ const readSwitches = () => (existsSync(switchesFile) ? JSON.parse(readFileSync(switchesFile, "utf8")) : {});
111
+
112
+ const threadsDir = join(HOME, "threads");
113
+ mkdirSync(threadsDir, { recursive: true });
114
+ let thread = null;
115
+ const saveThread = () => writeFileSync(join(threadsDir, `${thread.id}.json`), JSON.stringify(thread));
116
+ let current = null;
117
+ let turnCount = 0;
118
+
119
+ function reply(id, result) {
120
+ out({ id, result });
121
+ }
122
+
123
+ async function handle(msg) {
124
+ const { id, method, params } = msg;
125
+ switch (method) {
126
+ case "initialize":
127
+ return reply(id, { userAgent: "fake-codex/0.164.0" });
128
+ case "account/read":
129
+ return reply(id, existsSync(authFile) ? { account: { type: "chatgpt", email: basename(HOME), planType: "plus" }, requiresOpenaiAuth: true } : { account: null, requiresOpenaiAuth: true });
130
+ case "skills/extraRoots/set":
131
+ extraRoots = params.extraRoots ?? [];
132
+ return reply(id, {});
133
+ case "skills/list": {
134
+ // A bundled skill, and those found in the extra roots; switches persist in the account's config.
135
+ const found = [{ name: "imagegen", path: "/bundled/imagegen/SKILL.md" }];
136
+ for (const root of extraRoots)
137
+ for (const entry of existsSync(root) ? readdirSync(root) : []) if (existsSync(join(root, entry, "SKILL.md"))) found.push({ name: entry, path: join(root, entry, "SKILL.md") });
138
+ const switches = readSwitches();
139
+ return reply(id, { data: [{ cwd: params.cwds?.[0], skills: found.map((sk) => ({ ...sk, enabled: switches[sk.path] ?? true })) }] });
140
+ }
141
+ case "skills/config/write":
142
+ writeFileSync(switchesFile, JSON.stringify({ ...readSwitches(), [params.path]: params.enabled }));
143
+ return reply(id, {});
144
+ case "model/list":
145
+ return reply(id, { data: [{ id: "gpt-fake", displayName: "GPT Fake", isDefault: true, hidden: false, supportedReasoningEfforts: [{ reasoningEffort: "low" }, { reasoningEffort: "high" }] }, { id: "hidden", hidden: true }] });
146
+ case "account/rateLimits/read":
147
+ return reply(id, { rateLimits: { primary: { usedPercent: 42, windowDurationMins: 300, resetsAt: 2000000000 }, secondary: { usedPercent: 10, windowDurationMins: 10080 } } });
148
+ case "thread/start":
149
+ thread = { id: randomUUID(), turns: [], instructions: params.baseInstructions ?? null, developer: params.developerInstructions ?? null, sandbox: params.sandbox, approvalPolicy: params.approvalPolicy };
150
+ saveThread();
151
+ return reply(id, { thread: { id: thread.id }, model: params.model ?? "gpt-fake" });
152
+ case "thread/resume": {
153
+ const file = join(threadsDir, `${params.threadId}.json`);
154
+ if (!existsSync(file)) return out({ id, error: { code: -32600, message: "no such thread" } });
155
+ thread = JSON.parse(readFileSync(file, "utf8"));
156
+ // Like Codex 0.164, a resumed thread replays the last turn's usage, later on.
157
+ thread.replay = thread.lastTurnId;
158
+ return reply(id, { thread: { id: thread.id }, model: "gpt-fake" });
159
+ }
160
+ case "turn/start": {
161
+ const turn = { id: `turn_${++turnCount}_${randomUUID().slice(0, 8)}`, interrupted: false, model: params.model ?? null, effort: params.effort ?? null };
162
+ current = turn;
163
+ if (thread.replay) {
164
+ notify("thread/tokenUsage/updated", { threadId: thread.id, turnId: thread.replay, tokenUsage: { last: { inputTokens: 999, outputTokens: 0, cachedInputTokens: 0, reasoningOutputTokens: 0, totalTokens: 999 }, total: { inputTokens: 0, outputTokens: 0, cachedInputTokens: 0, reasoningOutputTokens: 0, totalTokens: 0 } } });
165
+ thread.replay = null;
166
+ }
167
+ // The real server announces the turn around the time it answers.
168
+ notify("turn/started", { threadId: thread.id, turn: { id: turn.id, status: "inProgress" } });
169
+ reply(id, { turn: { id: turn.id, status: "inProgress" } });
170
+ runTurn(turn, params.input?.[0]?.text ?? "").catch((e) => notify("turn/completed", { threadId: thread.id, turn: { id: turn.id, status: "failed", error: { message: String(e) } } }));
171
+ return;
172
+ }
173
+ case "turn/steer": {
174
+ if (!current || current.id !== params.expectedTurnId) return out({ id, error: { code: -32600, message: "no such active turn" } });
175
+ reply(id, { turnId: current.id });
176
+ current.steer?.(params.input?.[0]?.text ?? "");
177
+ return;
178
+ }
179
+ case "turn/interrupt":
180
+ if (current && current.id === params.turnId) current.interrupted = true;
181
+ current?.steer?.(null);
182
+ for (const [key, resolve] of waiting) {
183
+ waiting.delete(key);
184
+ resolve(null);
185
+ }
186
+ return reply(id, {});
187
+ default:
188
+ return out({ id, error: { code: -32601, message: `unknown method ${method}` } });
189
+ }
190
+ }
191
+
192
+ async function runTurn(turn, text) {
193
+ const base = { threadId: thread.id, turnId: turn.id };
194
+ const say = (delta) => notify("item/agentMessage/delta", { ...base, itemId: "msg", delta });
195
+ const end = (status, error) => {
196
+ if (current === turn) current = null;
197
+ notify("turn/completed", { threadId: thread.id, turn: { id: turn.id, status, ...(error ? { error } : {}) } });
198
+ };
199
+ notify("item/reasoning/summaryTextDelta", { ...base, itemId: "r", delta: "thinking" });
200
+
201
+ let answer;
202
+ const tool = /\btool (\w+)(?: (\{.*\}))?/.exec(text);
203
+ const mcp = /\bmcp ([\w-]+) (\w+)(?: (\{.*\}))?/.exec(text);
204
+ if (/crash/.test(text)) {
205
+ say("about to crash");
206
+ await sleep(20);
207
+ process.exit(70);
208
+ } else if (/limit/.test(text)) {
209
+ notify("error", { ...base, willRetry: false, error: { message: "You've hit your usage limit", codexErrorInfo: "usageLimitExceeded" } });
210
+ return end("failed", { message: "You've hit your usage limit", codexErrorInfo: "usageLimitExceeded" });
211
+ } else if (/^skill /.test(text)) {
212
+ const [, name, file] = text.split(" ");
213
+ const result = await callTool("read_skill", { skill: name, ...(file ? { file } : {}) });
214
+ answer = `${result.isError ? "error: " : ""}${result.text}`;
215
+ } else if (/^developer$/.test(text)) {
216
+ answer = thread.developer ?? "none";
217
+ } else if (mcp) {
218
+ notify("item/started", { ...base, item: { id: "m", type: "mcpToolCall", server: mcp[1], tool: mcp[2] } });
219
+ const result = await callTool(mcp[2], mcp[3] ? JSON.parse(mcp[3]) : {}, mcp[1]);
220
+ answer = `${mcp[1]}.${mcp[2]} said: ${result.text}${result.isError ? " (error)" : ""}`;
221
+ } else if (tool) {
222
+ notify("item/started", { ...base, item: { id: "t", type: "mcpToolCall", server: "edukors", tool: tool[1] } });
223
+ const result = await callTool(tool[1], tool[2] ? JSON.parse(tool[2]) : {});
224
+ answer = `Tool ${tool[1]} said: ${result.text}${result.isError ? " (error)" : ""}`;
225
+ } else if (/\bask\b/.test(text) && !/ask-blocking/.test(text)) {
226
+ const item = { type: "agentMessage", id: "q", text: "Which color?\n- red\n- blue", phase: "final_answer", delivery: "async", questions: [{ title: "Which color?", options: ["red", "blue"] }] };
227
+ notify("item/started", { ...base, item });
228
+ notify("item/completed", { ...base, item });
229
+ notify("item/started", { ...base, item: { type: "sleep", id: "s", durationMs: 43200000 } });
230
+ const steered = await new Promise((resolve) => (turn.steer = resolve));
231
+ if (steered === null || turn.interrupted) return end("interrupted");
232
+ answer = `You chose ${steered}`;
233
+ } else if (/ask-blocking/.test(text)) {
234
+ const res = await askClient("item/tool/requestUserInput", { ...base, itemId: "q", isBlocking: true, questions: [{ id: "color", header: "Color", question: "Which color?", options: [{ label: "red", description: "" }, { label: "blue", description: "" }] }] });
235
+ if (!res || turn.interrupted) return end("interrupted");
236
+ answer = `You chose ${res.answers?.color?.answers?.[0] ?? "nothing"}`;
237
+ } else if (/comando/.test(text)) {
238
+ const res = await askClient("item/commandExecution/requestApproval", { ...base, itemId: "c", command: "ls" });
239
+ answer = `Command ${res?.decision}`;
240
+ } else if (/slow|late/.test(text)) {
241
+ for (let i = 0; i < 40; i++) {
242
+ if (turn.interrupted) {
243
+ end("interrupted");
244
+ if (/late/.test(text)) {
245
+ await sleep(30);
246
+ say("LATE OUTPUT");
247
+ }
248
+ return;
249
+ }
250
+ say(".");
251
+ await sleep(50);
252
+ }
253
+ answer = "done slowly";
254
+ } else if (/\benv\b/.test(text)) {
255
+ answer = `OPENAI_API_KEY=${process.env.OPENAI_API_KEY ? "present" : "absent"} home=${basename(HOME)} sandbox=${thread.sandbox} approval=${thread.approvalPolicy}`;
256
+ } else {
257
+ answer = `Echo: ${text} [history ${thread.turns.length}, model ${turn.model ?? "default"}, effort ${turn.effort ?? "default"}, mcp ${mcpUrl ? "on" : "off"}, instructions ${thread.instructions ?? "none"}]`;
258
+ }
259
+ for (const piece of [answer.slice(0, 10), answer.slice(10)]) if (piece) say(piece);
260
+ thread.turns.push(text);
261
+ thread.lastTurnId = turn.id;
262
+ saveThread();
263
+ notify("thread/tokenUsage/updated", { ...base, tokenUsage: { last: { inputTokens: text.length, outputTokens: answer.length, cachedInputTokens: 0, reasoningOutputTokens: 0, totalTokens: text.length + answer.length }, total: { inputTokens: 0, outputTokens: 0, cachedInputTokens: 0, reasoningOutputTokens: 0, totalTokens: 0 } } });
264
+ end("completed");
265
+ }
266
+
267
+ createInterface({ input: process.stdin }).on("line", (line) => {
268
+ let msg;
269
+ try {
270
+ msg = JSON.parse(line);
271
+ } catch {
272
+ return;
273
+ }
274
+ if (msg.id !== undefined && msg.method === undefined) {
275
+ const resolve = waiting.get(msg.id);
276
+ waiting.delete(msg.id);
277
+ resolve?.(msg.result ?? null);
278
+ return;
279
+ }
280
+ if (msg.method && msg.id !== undefined) void handle(msg);
281
+ });
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ // A user's MCP server, for the tests: stdio, two tools. `lookup` answers,
3
+ // `broken` fails. FAKE_MCP_SECRET, when set in the server's own env, is
4
+ // echoed so a test can tell the configured environment reached it.
5
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
+ import { z } from "zod";
8
+
9
+ const server = new McpServer({ name: "glossary", version: "1.0.0" });
10
+ server.registerTool("lookup", { description: "Looks a term up in the glossary.", inputSchema: { term: z.string() } }, async ({ term }) => ({
11
+ content: [{ type: "text", text: `${term}: a term from the glossary${process.env.FAKE_MCP_SECRET ? ` (env ${process.env.FAKE_MCP_SECRET})` : ""}` }],
12
+ }));
13
+ server.registerTool("broken", { description: "Always fails.", inputSchema: {} }, async () => ({ content: [{ type: "text", text: "the glossary is offline" }], isError: true }));
14
+ await server.connect(new StdioServerTransport());
package/package.json CHANGED
@@ -1,6 +1,76 @@
1
1
  {
2
2
  "name": "@edukors/ai-cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "Shared integration between EDUKORS editors and installed AI provider CLIs: accounts, sessions, tools and normalized events.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "engines": {
8
+ "node": ">=22.12"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/mgarlabx/edukors_ai_cli.git"
13
+ },
14
+ "homepage": "https://github.com/mgarlabx/edukors_ai_cli#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/mgarlabx/edukors_ai_cli/issues"
17
+ },
18
+ "keywords": [
19
+ "edukors",
20
+ "claude",
21
+ "codex",
22
+ "ai",
23
+ "cli",
24
+ "agent",
25
+ "mcp"
26
+ ],
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "import": "./dist/index.js"
31
+ },
32
+ "./testing": {
33
+ "types": "./dist/testing/index.d.ts",
34
+ "import": "./dist/testing/index.js"
35
+ },
36
+ "./host": {
37
+ "types": "./dist/host/stdio.d.ts",
38
+ "import": "./dist/host/stdio.js"
39
+ },
40
+ "./settings": {
41
+ "types": "./dist/settings.d.ts",
42
+ "import": "./dist/settings.js"
43
+ }
44
+ },
45
+ "bin": {
46
+ "edukors-ai": "dist/cli/main.js",
47
+ "edukors-ai-host": "dist/host/main.js"
48
+ },
49
+ "files": [
50
+ "dist",
51
+ "fixtures",
52
+ "README.md",
53
+ "LICENSE"
54
+ ],
55
+ "publishConfig": {
56
+ "access": "public"
57
+ },
58
+ "scripts": {
59
+ "build": "tsc -p tsconfig.build.json",
60
+ "typecheck": "tsc --noEmit",
61
+ "test": "vitest run",
62
+ "cli": "npm run build --silent && node dist/cli/main.js",
63
+ "prepare": "tsc -p tsconfig.build.json",
64
+ "prepublishOnly": "npm run typecheck && npm test"
65
+ },
66
+ "dependencies": {
67
+ "@anthropic-ai/claude-agent-sdk": "0.3.287",
68
+ "@modelcontextprotocol/sdk": "^1.31.0",
69
+ "zod": "^4.6.5"
70
+ },
71
+ "devDependencies": {
72
+ "@types/node": "^22.19.0",
73
+ "typescript": "~6.0.3",
74
+ "vitest": "^5.0.3"
75
+ }
76
+ }