@alashi/provider-claude 0.1.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.
@@ -0,0 +1,18 @@
1
+ import { AIProvider, AISession, ChatMessage, SessionOptions } from "@alashi/apps-sdk";
2
+ //#region src/claude-provider.d.ts
3
+ interface ClaudeCodeProviderOptions {
4
+ /** Defaults applied to every session; per-session options win. */
5
+ sessionDefaults?: Partial<SessionOptions>;
6
+ /** Where Claude Code stores session transcripts (for history reads). */
7
+ claudeProjectsDir?: string;
8
+ }
9
+ declare class ClaudeCodeProvider implements AIProvider {
10
+ #private;
11
+ readonly name: string;
12
+ constructor(options?: ClaudeCodeProviderOptions);
13
+ createSession(options?: SessionOptions): Promise<AISession>;
14
+ history(sessionId: string): Promise<ChatMessage[]>;
15
+ }
16
+ //#endregion
17
+ export { ClaudeCodeProvider, type ClaudeCodeProviderOptions };
18
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/claude-provider.ts"],"mappings":";;UAoBiB;;EAEf,kBAAkB,QAAQ;;EAE1B;;cAiGW,8BAA8B;;WAChC;EAIT,YAAY,UAAS;EAKrB,cAAoB,UAAS,iBAAsB,QAAQ;EAQ3D,QAAc,oBAAoB,QAAQ"}
package/dist/index.mjs ADDED
@@ -0,0 +1,346 @@
1
+ import { AppSdkError } from "@alashi/apps-sdk";
2
+ import { query } from "@anthropic-ai/claude-agent-sdk";
3
+ import { mkdir, readFile, readdir, rm, stat, symlink, writeFile } from "node:fs/promises";
4
+ import { homedir, tmpdir } from "node:os";
5
+ import { basename, join } from "node:path";
6
+ import { createHash } from "node:crypto";
7
+ //#region src/event-mapping.ts
8
+ /**
9
+ * Maps a raw Agent SDK message to zero or more provider-neutral events.
10
+ * With includePartialMessages on, text/thinking arrive as stream_event
11
+ * deltas, so the whole-message assistant blocks are skipped to avoid
12
+ * emitting the same text twice.
13
+ */
14
+ function mapMessage(message) {
15
+ switch (message.type) {
16
+ case "system":
17
+ if (message.subtype === "init") return [{
18
+ type: "session",
19
+ sessionId: message.session_id,
20
+ model: message.model
21
+ }];
22
+ return [];
23
+ case "stream_event": {
24
+ const event = message.event;
25
+ if (event.type === "content_block_delta") {
26
+ if (event.delta.type === "text_delta") return [{
27
+ type: "text",
28
+ text: event.delta.text
29
+ }];
30
+ if (event.delta.type === "thinking_delta") return [{
31
+ type: "thinking",
32
+ text: event.delta.thinking
33
+ }];
34
+ }
35
+ return [];
36
+ }
37
+ case "assistant": {
38
+ const events = [];
39
+ for (const block of message.message.content) if (block.type === "tool_use") events.push({
40
+ type: "tool_use",
41
+ name: block.name,
42
+ input: block.input
43
+ });
44
+ return events;
45
+ }
46
+ case "user": {
47
+ const content = message.message.content;
48
+ if (!Array.isArray(content)) return [];
49
+ const events = [];
50
+ for (const block of content) if (typeof block === "object" && block !== null && block.type === "tool_result") events.push({
51
+ type: "tool_result",
52
+ output: block.content,
53
+ isError: block.is_error ?? false
54
+ });
55
+ return events;
56
+ }
57
+ case "result":
58
+ if (message.subtype === "success") return [{
59
+ type: "result",
60
+ ok: !message.is_error,
61
+ text: message.result,
62
+ costUsd: message.total_cost_usd,
63
+ turns: message.num_turns
64
+ }];
65
+ return [{
66
+ type: "result",
67
+ ok: false,
68
+ error: message.subtype,
69
+ costUsd: message.total_cost_usd,
70
+ turns: message.num_turns
71
+ }];
72
+ default: return [];
73
+ }
74
+ }
75
+ //#endregion
76
+ //#region src/history.ts
77
+ const SESSION_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
78
+ function defaultProjectsDir() {
79
+ return join(homedir(), ".claude", "projects");
80
+ }
81
+ /** Session transcripts are keyed by a munged cwd we don't know; scan instead. */
82
+ async function findSessionFile(projectsDir, sessionId) {
83
+ let dirs;
84
+ try {
85
+ dirs = await readdir(projectsDir);
86
+ } catch {
87
+ return;
88
+ }
89
+ for (const dir of dirs) {
90
+ const candidate = join(projectsDir, dir, `${sessionId}.jsonl`);
91
+ try {
92
+ await stat(candidate);
93
+ return candidate;
94
+ } catch {}
95
+ }
96
+ }
97
+ function textFromContent(content) {
98
+ if (typeof content === "string") return content;
99
+ if (!Array.isArray(content)) return "";
100
+ return content.filter((block) => typeof block === "object" && block !== null && block.type === "text" && typeof block.text === "string").map((block) => block.text).join("");
101
+ }
102
+ function toChatMessage(line) {
103
+ if (line.isMeta) return void 0;
104
+ if (line.type !== "user" && line.type !== "assistant") return void 0;
105
+ const role = line.type;
106
+ if (line.message?.role !== role) return void 0;
107
+ const text = textFromContent(line.message.content);
108
+ if (text === "") return void 0;
109
+ return {
110
+ role,
111
+ text,
112
+ ...line.timestamp && { timestamp: line.timestamp }
113
+ };
114
+ }
115
+ /**
116
+ * Reads a session's past messages from Claude Code's own transcript
117
+ * (~/.claude/projects/<project>/<sessionId>.jsonl). The JSONL layout is a
118
+ * Claude Code internal, so parsing is deliberately forgiving: unknown or
119
+ * malformed lines are skipped rather than failing the whole read.
120
+ */
121
+ async function readSessionHistory(sessionId, projectsDir = defaultProjectsDir()) {
122
+ if (!SESSION_ID_PATTERN.test(sessionId)) return [];
123
+ const file = await findSessionFile(projectsDir, sessionId);
124
+ if (!file) return [];
125
+ const messages = [];
126
+ for (const line of (await readFile(file, "utf8")).split("\n")) {
127
+ if (line.trim() === "") continue;
128
+ let parsed;
129
+ try {
130
+ parsed = JSON.parse(line);
131
+ } catch {
132
+ continue;
133
+ }
134
+ const message = toChatMessage(parsed);
135
+ if (message) messages.push(message);
136
+ }
137
+ return messages;
138
+ }
139
+ //#endregion
140
+ //#region src/message-queue.ts
141
+ /** Push-based async iterable used as the streaming-input prompt for query(). */
142
+ var AsyncMessageQueue = class {
143
+ #values = [];
144
+ #resolvers = [];
145
+ #done = false;
146
+ push(value) {
147
+ if (this.#done) throw new Error("Queue is closed");
148
+ const resolver = this.#resolvers.shift();
149
+ if (resolver) resolver({
150
+ value,
151
+ done: false
152
+ });
153
+ else this.#values.push(value);
154
+ }
155
+ end() {
156
+ if (this.#done) return;
157
+ this.#done = true;
158
+ for (const resolver of this.#resolvers.splice(0)) resolver({
159
+ value: void 0,
160
+ done: true
161
+ });
162
+ }
163
+ [Symbol.asyncIterator]() {
164
+ return { next: () => {
165
+ const value = this.#values.shift();
166
+ if (value !== void 0) return Promise.resolve({
167
+ value,
168
+ done: false
169
+ });
170
+ if (this.#done) return Promise.resolve({
171
+ value: void 0,
172
+ done: true
173
+ });
174
+ return new Promise((resolve) => {
175
+ this.#resolvers.push(resolve);
176
+ });
177
+ } };
178
+ }
179
+ };
180
+ //#endregion
181
+ //#region src/skills-plugin.ts
182
+ /**
183
+ * With settingSources [] the engine discovers skills only through plugins,
184
+ * so app-declared skill dirs are wrapped in a synthesized local plugin whose
185
+ * skills/ dir symlinks them. Each declared dir may be a single skill
186
+ * (contains SKILL.md) or a parent holding several skill folders.
187
+ */
188
+ const cache = /* @__PURE__ */ new Map();
189
+ async function isSkill(dir) {
190
+ return stat(join(dir, "SKILL.md")).then((s) => s.isFile(), () => false);
191
+ }
192
+ async function build(pluginDir, skillDirs) {
193
+ await rm(pluginDir, {
194
+ recursive: true,
195
+ force: true
196
+ });
197
+ const skillsDir = join(pluginDir, "skills");
198
+ await mkdir(join(pluginDir, ".claude-plugin"), { recursive: true });
199
+ await mkdir(skillsDir, { recursive: true });
200
+ await writeFile(join(pluginDir, ".claude-plugin", "plugin.json"), JSON.stringify({
201
+ name: "alashi-app-skills",
202
+ description: "Skills declared by the alashi app manifest"
203
+ }), "utf8");
204
+ for (const dir of skillDirs) {
205
+ if (await isSkill(dir)) {
206
+ await symlink(dir, join(skillsDir, basename(dir)), "dir");
207
+ continue;
208
+ }
209
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
210
+ const child = join(dir, entry.name);
211
+ if (await isSkill(child)) await symlink(child, join(skillsDir, entry.name), "dir");
212
+ }
213
+ }
214
+ return pluginDir;
215
+ }
216
+ /** Plugin dir for a set of skill dirs; built once per process per set. */
217
+ function ensureSkillsPlugin(skillDirs) {
218
+ const key = createHash("sha256").update(JSON.stringify([...skillDirs].sort())).digest("hex").slice(0, 12);
219
+ let plugin = cache.get(key);
220
+ if (!plugin) {
221
+ plugin = build(join(tmpdir(), `alashi-skills-${key}`), skillDirs);
222
+ plugin.catch(() => cache.delete(key));
223
+ cache.set(key, plugin);
224
+ }
225
+ return plugin;
226
+ }
227
+ //#endregion
228
+ //#region src/claude-provider.ts
229
+ function toQueryOptions(options, skillsPluginDir) {
230
+ const out = {
231
+ permissionMode: "dontAsk",
232
+ settingSources: [],
233
+ includePartialMessages: true
234
+ };
235
+ if (options.systemPrompt !== void 0) out.systemPrompt = options.systemPrompt;
236
+ if (options.model !== void 0) out.model = options.model;
237
+ if (options.mcpServers !== void 0) out.mcpServers = options.mcpServers;
238
+ if (options.allowedTools !== void 0) out.allowedTools = options.allowedTools;
239
+ if (options.maxTurns !== void 0) out.maxTurns = options.maxTurns;
240
+ if (options.cwd !== void 0) out.cwd = options.cwd;
241
+ if (options.resume !== void 0) out.resume = options.resume;
242
+ if (options.agents !== void 0 && Object.keys(options.agents).length > 0) {
243
+ out.agents = options.agents;
244
+ out.allowedTools = [.../* @__PURE__ */ new Set([...out.allowedTools ?? [], "Agent"])];
245
+ }
246
+ if (skillsPluginDir !== void 0) {
247
+ out.plugins = [{
248
+ type: "local",
249
+ path: skillsPluginDir
250
+ }];
251
+ out.skills = "all";
252
+ }
253
+ return out;
254
+ }
255
+ /**
256
+ * One long-lived Claude Code session per chat (streaming input mode), so
257
+ * follow-up turns reuse the process and its prompt cache instead of paying
258
+ * session startup per message. Turns are serialized: one send() at a time.
259
+ */
260
+ var ClaudeSession = class {
261
+ #queue = new AsyncMessageQueue();
262
+ #query;
263
+ #iterator;
264
+ #id = "";
265
+ #busy = false;
266
+ #closed = false;
267
+ constructor(options, skillsPluginDir) {
268
+ this.#query = query({
269
+ prompt: this.#queue,
270
+ options: toQueryOptions(options, skillsPluginDir)
271
+ });
272
+ this.#iterator = this.#query[Symbol.asyncIterator]();
273
+ }
274
+ get id() {
275
+ return this.#id;
276
+ }
277
+ send(input) {
278
+ if (this.#closed) throw new AppSdkError("Session is closed");
279
+ if (this.#busy) throw new AppSdkError("Session is busy with another turn");
280
+ this.#busy = true;
281
+ return this.#run(input);
282
+ }
283
+ async *#run(input) {
284
+ try {
285
+ this.#queue.push({
286
+ type: "user",
287
+ message: {
288
+ role: "user",
289
+ content: input
290
+ },
291
+ parent_tool_use_id: null
292
+ });
293
+ while (true) {
294
+ const next = await this.#iterator.next();
295
+ if (next.done) {
296
+ yield {
297
+ type: "result",
298
+ ok: false,
299
+ error: "Session ended unexpectedly"
300
+ };
301
+ return;
302
+ }
303
+ const message = next.value;
304
+ for (const event of mapMessage(message)) {
305
+ if (event.type === "session") this.#id = event.sessionId;
306
+ yield event;
307
+ }
308
+ if (message.type === "result") return;
309
+ }
310
+ } finally {
311
+ this.#busy = false;
312
+ }
313
+ }
314
+ async interrupt() {
315
+ await this.#query.interrupt();
316
+ }
317
+ async close() {
318
+ if (this.#closed) return;
319
+ this.#closed = true;
320
+ this.#queue.end();
321
+ await this.#iterator.return?.(void 0)?.catch(() => void 0);
322
+ }
323
+ };
324
+ var ClaudeCodeProvider = class {
325
+ name = "claude-code";
326
+ #defaults;
327
+ #projectsDir;
328
+ constructor(options = {}) {
329
+ this.#defaults = options.sessionDefaults ?? {};
330
+ this.#projectsDir = options.claudeProjectsDir;
331
+ }
332
+ async createSession(options = {}) {
333
+ const merged = {
334
+ ...this.#defaults,
335
+ ...options
336
+ };
337
+ return new ClaudeSession(merged, merged.skillDirs?.length ? await ensureSkillsPlugin(merged.skillDirs) : void 0);
338
+ }
339
+ async history(sessionId) {
340
+ return readSessionHistory(sessionId, this.#projectsDir ?? defaultProjectsDir());
341
+ }
342
+ };
343
+ //#endregion
344
+ export { ClaudeCodeProvider };
345
+
346
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":["#done","#resolvers","#values","#query","#queue","#iterator","#id","#closed","#busy","#run","#defaults","#projectsDir"],"sources":["../src/event-mapping.ts","../src/history.ts","../src/message-queue.ts","../src/skills-plugin.ts","../src/claude-provider.ts"],"sourcesContent":["import type { AgentEvent } from '@alashi/apps-sdk';\nimport type { SDKMessage } from '@anthropic-ai/claude-agent-sdk';\n\n/**\n * Maps a raw Agent SDK message to zero or more provider-neutral events.\n * With includePartialMessages on, text/thinking arrive as stream_event\n * deltas, so the whole-message assistant blocks are skipped to avoid\n * emitting the same text twice.\n */\nexport function mapMessage(message: SDKMessage): AgentEvent[] {\n switch (message.type) {\n case 'system': {\n if (message.subtype === 'init') {\n return [{ type: 'session', sessionId: message.session_id, model: message.model }];\n }\n return [];\n }\n case 'stream_event': {\n const event = message.event;\n if (event.type === 'content_block_delta') {\n if (event.delta.type === 'text_delta') {\n return [{ type: 'text', text: event.delta.text }];\n }\n if (event.delta.type === 'thinking_delta') {\n return [{ type: 'thinking', text: event.delta.thinking }];\n }\n }\n return [];\n }\n case 'assistant': {\n const events: AgentEvent[] = [];\n for (const block of message.message.content) {\n if (block.type === 'tool_use') {\n events.push({ type: 'tool_use', name: block.name, input: block.input });\n }\n }\n return events;\n }\n case 'user': {\n const content = message.message.content;\n if (!Array.isArray(content)) return [];\n const events: AgentEvent[] = [];\n for (const block of content) {\n if (typeof block === 'object' && block !== null && block.type === 'tool_result') {\n events.push({\n type: 'tool_result',\n output: block.content,\n isError: block.is_error ?? false,\n });\n }\n }\n return events;\n }\n case 'result': {\n if (message.subtype === 'success') {\n return [\n {\n type: 'result',\n ok: !message.is_error,\n text: message.result,\n costUsd: message.total_cost_usd,\n turns: message.num_turns,\n },\n ];\n }\n return [\n {\n type: 'result',\n ok: false,\n error: message.subtype,\n costUsd: message.total_cost_usd,\n turns: message.num_turns,\n },\n ];\n }\n default:\n return [];\n }\n}\n","import { readdir, readFile, stat } from 'node:fs/promises';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport type { ChatMessage } from '@alashi/apps-sdk';\n\n// Also a traversal guard: sessionId is used as a filename.\nconst SESSION_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;\n\ninterface TranscriptLine {\n type?: string;\n isMeta?: boolean;\n timestamp?: string;\n message?: {\n role?: string;\n content?: unknown;\n };\n}\n\nexport function defaultProjectsDir(): string {\n return join(homedir(), '.claude', 'projects');\n}\n\n/** Session transcripts are keyed by a munged cwd we don't know; scan instead. */\nasync function findSessionFile(\n projectsDir: string,\n sessionId: string,\n): Promise<string | undefined> {\n let dirs: string[];\n try {\n dirs = await readdir(projectsDir);\n } catch {\n return undefined;\n }\n for (const dir of dirs) {\n const candidate = join(projectsDir, dir, `${sessionId}.jsonl`);\n try {\n await stat(candidate);\n return candidate;\n } catch {\n // Not in this project dir; keep scanning.\n }\n }\n return undefined;\n}\n\nfunction textFromContent(content: unknown): string {\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return '';\n return content\n .filter(\n (block): block is { type: 'text'; text: string } =>\n typeof block === 'object' &&\n block !== null &&\n (block as { type?: string }).type === 'text' &&\n typeof (block as { text?: unknown }).text === 'string',\n )\n .map((block) => block.text)\n .join('');\n}\n\nfunction toChatMessage(line: TranscriptLine): ChatMessage | undefined {\n if (line.isMeta) return undefined;\n if (line.type !== 'user' && line.type !== 'assistant') return undefined;\n const role = line.type;\n if (line.message?.role !== role) return undefined;\n const text = textFromContent(line.message.content);\n // Tool-result user lines and tool-use-only assistant lines have no text.\n if (text === '') return undefined;\n return { role, text, ...(line.timestamp && { timestamp: line.timestamp }) };\n}\n\n/**\n * Reads a session's past messages from Claude Code's own transcript\n * (~/.claude/projects/<project>/<sessionId>.jsonl). The JSONL layout is a\n * Claude Code internal, so parsing is deliberately forgiving: unknown or\n * malformed lines are skipped rather than failing the whole read.\n */\nexport async function readSessionHistory(\n sessionId: string,\n projectsDir: string = defaultProjectsDir(),\n): Promise<ChatMessage[]> {\n if (!SESSION_ID_PATTERN.test(sessionId)) return [];\n const file = await findSessionFile(projectsDir, sessionId);\n if (!file) return [];\n\n const messages: ChatMessage[] = [];\n for (const line of (await readFile(file, 'utf8')).split('\\n')) {\n if (line.trim() === '') continue;\n let parsed: TranscriptLine;\n try {\n parsed = JSON.parse(line) as TranscriptLine;\n } catch {\n continue;\n }\n const message = toChatMessage(parsed);\n if (message) messages.push(message);\n }\n return messages;\n}\n","/** Push-based async iterable used as the streaming-input prompt for query(). */\nexport class AsyncMessageQueue<T> implements AsyncIterable<T> {\n #values: T[] = [];\n #resolvers: Array<(result: IteratorResult<T>) => void> = [];\n #done = false;\n\n push(value: T): void {\n if (this.#done) throw new Error('Queue is closed');\n const resolver = this.#resolvers.shift();\n if (resolver) resolver({ value, done: false });\n else this.#values.push(value);\n }\n\n end(): void {\n if (this.#done) return;\n this.#done = true;\n for (const resolver of this.#resolvers.splice(0)) {\n resolver({ value: undefined, done: true });\n }\n }\n\n [Symbol.asyncIterator](): AsyncIterator<T> {\n return {\n next: (): Promise<IteratorResult<T>> => {\n const value = this.#values.shift();\n if (value !== undefined) return Promise.resolve({ value, done: false });\n if (this.#done) return Promise.resolve({ value: undefined, done: true });\n return new Promise((resolve) => {\n this.#resolvers.push(resolve);\n });\n },\n };\n }\n}\n","import { createHash } from 'node:crypto';\nimport { mkdir, readdir, rm, stat, symlink, writeFile } from 'node:fs/promises';\nimport { tmpdir } from 'node:os';\nimport { basename, join } from 'node:path';\n\n/**\n * With settingSources [] the engine discovers skills only through plugins,\n * so app-declared skill dirs are wrapped in a synthesized local plugin whose\n * skills/ dir symlinks them. Each declared dir may be a single skill\n * (contains SKILL.md) or a parent holding several skill folders.\n */\n\nconst cache = new Map<string, Promise<string>>();\n\nasync function isSkill(dir: string): Promise<boolean> {\n return stat(join(dir, 'SKILL.md')).then(\n (s) => s.isFile(),\n () => false,\n );\n}\n\nasync function build(pluginDir: string, skillDirs: string[]): Promise<string> {\n // Rebuild from scratch: symlinks are cheap and stale links are worse.\n await rm(pluginDir, { recursive: true, force: true });\n const skillsDir = join(pluginDir, 'skills');\n await mkdir(join(pluginDir, '.claude-plugin'), { recursive: true });\n await mkdir(skillsDir, { recursive: true });\n await writeFile(\n join(pluginDir, '.claude-plugin', 'plugin.json'),\n JSON.stringify({\n name: 'alashi-app-skills',\n description: 'Skills declared by the alashi app manifest',\n }),\n 'utf8',\n );\n for (const dir of skillDirs) {\n if (await isSkill(dir)) {\n await symlink(dir, join(skillsDir, basename(dir)), 'dir');\n continue;\n }\n for (const entry of await readdir(dir, { withFileTypes: true })) {\n const child = join(dir, entry.name);\n if (await isSkill(child)) await symlink(child, join(skillsDir, entry.name), 'dir');\n }\n }\n return pluginDir;\n}\n\n/** Plugin dir for a set of skill dirs; built once per process per set. */\nexport function ensureSkillsPlugin(skillDirs: string[]): Promise<string> {\n const key = createHash('sha256')\n .update(JSON.stringify([...skillDirs].sort()))\n .digest('hex')\n .slice(0, 12);\n let plugin = cache.get(key);\n if (!plugin) {\n plugin = build(join(tmpdir(), `alashi-skills-${key}`), skillDirs);\n plugin.catch(() => cache.delete(key));\n cache.set(key, plugin);\n }\n return plugin;\n}\n","import {\n type AgentEvent,\n type AIProvider,\n type AISession,\n AppSdkError,\n type ChatMessage,\n type SessionOptions,\n} from '@alashi/apps-sdk';\nimport {\n type Options,\n type Query,\n query,\n type SDKMessage,\n type SDKUserMessage,\n} from '@anthropic-ai/claude-agent-sdk';\nimport { mapMessage } from './event-mapping.js';\nimport { defaultProjectsDir, readSessionHistory } from './history.js';\nimport { AsyncMessageQueue } from './message-queue.js';\nimport { ensureSkillsPlugin } from './skills-plugin.js';\n\nexport interface ClaudeCodeProviderOptions {\n /** Defaults applied to every session; per-session options win. */\n sessionDefaults?: Partial<SessionOptions>;\n /** Where Claude Code stores session transcripts (for history reads). */\n claudeProjectsDir?: string;\n}\n\nfunction toQueryOptions(options: SessionOptions, skillsPluginDir?: string): Options {\n const out: Options = {\n // Non-interactive: manifest-declared allowedTools are approved, everything else denied.\n permissionMode: 'dontAsk',\n // Apps run isolated from the user's personal/project Claude Code settings.\n settingSources: [],\n // Text arrives as stream_event deltas (see event-mapping.ts).\n includePartialMessages: true,\n };\n if (options.systemPrompt !== undefined) out.systemPrompt = options.systemPrompt;\n if (options.model !== undefined) out.model = options.model;\n if (options.mcpServers !== undefined) out.mcpServers = options.mcpServers;\n if (options.allowedTools !== undefined) out.allowedTools = options.allowedTools;\n if (options.maxTurns !== undefined) out.maxTurns = options.maxTurns;\n if (options.cwd !== undefined) out.cwd = options.cwd;\n if (options.resume !== undefined) out.resume = options.resume;\n if (options.agents !== undefined && Object.keys(options.agents).length > 0) {\n out.agents = options.agents;\n // Subagents are invoked through the Agent tool; approve it alongside them.\n out.allowedTools = [...new Set([...(out.allowedTools ?? []), 'Agent'])];\n }\n if (skillsPluginDir !== undefined) {\n out.plugins = [{ type: 'local', path: skillsPluginDir }];\n out.skills = 'all';\n }\n return out;\n}\n\n/**\n * One long-lived Claude Code session per chat (streaming input mode), so\n * follow-up turns reuse the process and its prompt cache instead of paying\n * session startup per message. Turns are serialized: one send() at a time.\n */\nclass ClaudeSession implements AISession {\n #queue: AsyncMessageQueue<SDKUserMessage> = new AsyncMessageQueue();\n #query: Query;\n #iterator: AsyncIterator<SDKMessage>;\n #id = '';\n #busy = false;\n #closed = false;\n\n constructor(options: SessionOptions, skillsPluginDir?: string) {\n this.#query = query({ prompt: this.#queue, options: toQueryOptions(options, skillsPluginDir) });\n this.#iterator = this.#query[Symbol.asyncIterator]();\n }\n\n get id(): string {\n return this.#id;\n }\n\n send(input: string): AsyncIterable<AgentEvent> {\n if (this.#closed) throw new AppSdkError('Session is closed');\n if (this.#busy) throw new AppSdkError('Session is busy with another turn');\n this.#busy = true;\n return this.#run(input);\n }\n\n async *#run(input: string): AsyncGenerator<AgentEvent> {\n try {\n this.#queue.push({\n type: 'user',\n message: { role: 'user', content: input },\n parent_tool_use_id: null,\n });\n while (true) {\n const next = await this.#iterator.next();\n if (next.done) {\n yield { type: 'result', ok: false, error: 'Session ended unexpectedly' };\n return;\n }\n const message = next.value;\n for (const event of mapMessage(message)) {\n if (event.type === 'session') this.#id = event.sessionId;\n yield event;\n }\n if (message.type === 'result') return;\n }\n } finally {\n this.#busy = false;\n }\n }\n\n async interrupt(): Promise<void> {\n await this.#query.interrupt();\n }\n\n async close(): Promise<void> {\n if (this.#closed) return;\n this.#closed = true;\n this.#queue.end();\n await this.#iterator.return?.(undefined)?.catch(() => undefined);\n }\n}\n\nexport class ClaudeCodeProvider implements AIProvider {\n readonly name: string = 'claude-code';\n readonly #defaults: Partial<SessionOptions>;\n readonly #projectsDir: string | undefined;\n\n constructor(options: ClaudeCodeProviderOptions = {}) {\n this.#defaults = options.sessionDefaults ?? {};\n this.#projectsDir = options.claudeProjectsDir;\n }\n\n async createSession(options: SessionOptions = {}): Promise<AISession> {\n const merged = { ...this.#defaults, ...options };\n const skillsPluginDir = merged.skillDirs?.length\n ? await ensureSkillsPlugin(merged.skillDirs)\n : undefined;\n return new ClaudeSession(merged, skillsPluginDir);\n }\n\n async history(sessionId: string): Promise<ChatMessage[]> {\n return readSessionHistory(sessionId, this.#projectsDir ?? defaultProjectsDir());\n }\n}\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@alashi/provider-claude",
3
+ "version": "0.1.0",
4
+ "description": "Claude Code AIProvider for alashi, built on the Claude Agent SDK",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/alashi-dev/alashi.git",
10
+ "directory": "packages/provider-claude"
11
+ },
12
+ "engines": {
13
+ "node": ">=22.12"
14
+ },
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.mts",
18
+ "default": "./dist/index.mjs"
19
+ }
20
+ },
21
+ "main": "./dist/index.mjs",
22
+ "types": "./dist/index.d.mts",
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "dependencies": {
27
+ "@alashi/apps-sdk": "^0.1.2",
28
+ "@anthropic-ai/claude-agent-sdk": "^0.3.232"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^26.2.0",
32
+ "tsdown": "^0.22.14",
33
+ "typescript": "^7.0.2",
34
+ "vitest": "^4.1.10"
35
+ },
36
+ "scripts": {
37
+ "build": "tsdown",
38
+ "test": "vitest run"
39
+ }
40
+ }