@pikiloom/kernel 0.2.2 → 0.2.4

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.
@@ -30,9 +30,20 @@ export declare function claudeUsageOf(s: ClaudeUsageState): UniversalUsage;
30
30
  export declare function claudeContextWindowFromModel(model: unknown): number | null;
31
31
  export declare function claudeEffectiveContextWindow(advertised: number | null): number | null;
32
32
  export declare function handleClaudeEvent(ev: any, s: any, emit: (e: DriverEvent) => void): void;
33
- export declare function claudeUserMessage(text: string): string;
33
+ export declare function claudeBgHoldCapMs(): number;
34
+ export declare function isTerminalTaskStatus(status: unknown): boolean;
35
+ export declare function trackClaudeBackgroundTask(ev: any, s: any): void;
36
+ export declare function pendingClaudeBackgroundTasks(s: any): number;
37
+ export type ClaudeResultSettleDecision = 'settle' | 'hold';
38
+ export declare function decideClaudeResultSettle(input: {
39
+ hasError: boolean;
40
+ pendingBackground: number;
41
+ }): ClaudeResultSettleDecision;
42
+ export declare function claudeUserMessage(text: string, attachments?: string[]): string;
34
43
  export declare function emitClaudeImages(blocks: any[], s: any, emit: (e: DriverEvent) => void): void;
35
44
  export declare function todoWriteToPlan(input: any): UniversalPlan | null;
45
+ export declare function readClaudeTaskCreateId(ev: any, block: any): string | null;
46
+ export declare function rebuildClaudeTaskPlan(s: any): UniversalPlan | null;
36
47
  export declare function shortToolValue(value: unknown, max?: number): string;
37
48
  export declare function summarizeToolUse(name: string, input: any): string;
38
49
  export declare function firstResultLine(content: any): string;
@@ -1,4 +1,6 @@
1
1
  import { spawn } from 'node:child_process';
2
+ import { readFileSync } from 'node:fs';
3
+ import { extname } from 'node:path';
2
4
  import { discoverClaudeNativeSessions } from '../workspace/native.js';
3
5
  // Real driver: shells the local `claude` CLI in stream-json mode and normalizes its
4
6
  // events into kernel DriverEvents. Faithful to pikiloom's claude.ts event shapes
@@ -28,7 +30,14 @@ export class ClaudeDriver {
28
30
  }
29
31
  run(input, ctx) {
30
32
  const steerable = !!input.steerable;
31
- const args = ['-p', '--verbose', '--output-format', 'stream-json', '--include-partial-messages'];
33
+ // Always drive Claude over a stream-json stdin and keep that stdin OPEN for the whole turn.
34
+ // That open stdin is what lets Claude's native "launch detached background work → end the
35
+ // turn → wake itself up and report when the work finishes" flow play out: it only happens
36
+ // while stdin stays open (with it closed, Claude exits at the first `result` and the wake-up
37
+ // — and any in-process background agent/workflow — is lost). Image attachments also need the
38
+ // stream-json user message (a plain text stdin can't carry images). `--replay-user-messages`
39
+ // + registerSteer remain the only mid-turn-steer-specific bits.
40
+ const args = ['-p', '--verbose', '--output-format', 'stream-json', '--include-partial-messages', '--input-format', 'stream-json'];
32
41
  if (input.model)
33
42
  args.push('--model', input.model);
34
43
  if (input.effort)
@@ -42,7 +51,7 @@ export class ClaudeDriver {
42
51
  if (input.permissionMode)
43
52
  args.push('--permission-mode', input.permissionMode); // parity: keep bypass/accept-edits on the kernel path
44
53
  if (steerable)
45
- args.push('--input-format', 'stream-json', '--replay-user-messages'); // parity: mid-turn steer
54
+ args.push('--replay-user-messages'); // parity: mid-turn steer
46
55
  if (input.extraArgs?.length)
47
56
  args.push(...input.extraArgs);
48
57
  const state = {
@@ -54,26 +63,57 @@ export class ClaudeDriver {
54
63
  contextWindow: null, turnOutputTokensBase: 0,
55
64
  subAgents: new Map(),
56
65
  tools: new Map(),
66
+ taskList: new Map(),
67
+ taskOrder: [],
68
+ pendingTaskCreates: new Map(),
69
+ // run_in_background lifecycle: task ids seen as started vs. reached a terminal status.
70
+ bgStarted: new Set(), bgTerminal: new Set(),
57
71
  };
58
72
  return new Promise((resolve) => {
59
73
  let child;
60
74
  let settled = false;
61
- const finish = (r) => {
75
+ let holdCapTimer = null;
76
+ const usageOf = () => this.usage(state);
77
+ const clearHoldCap = () => { if (holdCapTimer) {
78
+ clearTimeout(holdCapTimer);
79
+ holdCapTimer = null;
80
+ } };
81
+ // kill=true SIGTERMs immediately — fast exit, used once nothing is left running in the
82
+ // background (a normal turn, or a wake-up turn after every background task finished).
83
+ // kill=false only ends stdin and lets Claude shut down on its own, so any still-running
84
+ // detached background work survives a clean exit (a hard kill mid-flight is exactly what
85
+ // tore the background — and the wake-up — down before). A leak-guard SIGTERM is the backstop.
86
+ const finish = (r, kill = true) => {
62
87
  if (settled)
63
88
  return;
64
89
  settled = true;
90
+ clearHoldCap();
65
91
  try {
66
92
  child?.stdin?.end();
67
93
  }
68
94
  catch { /* ignore */ }
69
- if (steerable) {
70
- try {
71
- child?.kill('SIGTERM');
95
+ if (!child.killed && child.exitCode == null) {
96
+ if (kill) {
97
+ try {
98
+ child.kill('SIGTERM');
99
+ }
100
+ catch { /* ignore */ }
101
+ }
102
+ else {
103
+ const guard = setTimeout(() => { try {
104
+ child?.kill('SIGTERM');
105
+ }
106
+ catch { /* ignore */ } }, CLAUDE_EXIT_LEAK_GUARD_MS);
107
+ if (typeof guard.unref === 'function')
108
+ guard.unref();
72
109
  }
73
- catch { /* ignore */ }
74
- } // replay mode keeps the process alive past the turn
110
+ }
75
111
  resolve(r);
76
112
  };
113
+ const settleResult = (opts = {}) => finish({
114
+ ok: !state.error, text: state.text, reasoning: state.reasoning || undefined,
115
+ error: state.error, stopReason: opts.stopReason ?? state.stopReason, sessionId: state.sessionId, usage: usageOf(),
116
+ }, opts.kill ?? true);
77
117
  try {
78
118
  child = spawn(this.bin, args, { cwd: input.workdir, env: { ...process.env, ...(input.env || {}) }, stdio: ['pipe', 'pipe', 'pipe'] });
79
119
  }
@@ -90,9 +130,9 @@ export class ClaudeDriver {
90
130
  else
91
131
  ctx.signal.addEventListener('abort', onAbort, { once: true });
92
132
  if (steerable) {
93
- ctx.registerSteer(async (prompt) => {
133
+ ctx.registerSteer(async (prompt, attachments) => {
94
134
  try {
95
- child.stdin.write(claudeUserMessage(prompt) + '\n');
135
+ child.stdin.write(claudeUserMessage(prompt, attachments) + '\n');
96
136
  return true;
97
137
  }
98
138
  catch {
@@ -100,14 +140,17 @@ export class ClaudeDriver {
100
140
  }
101
141
  });
102
142
  }
103
- const usageOf = () => this.usage(state);
104
143
  let buf = '';
105
144
  let stderr = '';
106
145
  child.stdout.on('data', (chunk) => {
146
+ if (settled)
147
+ return; // ignore the process's post-settle shutdown chatter
107
148
  buf += chunk.toString('utf8');
108
149
  const lines = buf.split('\n');
109
150
  buf = lines.pop() || '';
110
151
  for (const line of lines) {
152
+ if (settled)
153
+ return;
111
154
  const trimmed = line.trim();
112
155
  if (!trimmed)
113
156
  continue;
@@ -119,29 +162,42 @@ export class ClaudeDriver {
119
162
  continue;
120
163
  }
121
164
  handleClaudeEvent(ev, state, ctx.emit);
122
- if (steerable && ev.type === 'result') {
123
- // In replay mode the turn ends on `result` but the process lingers; settle now.
124
- finish({ ok: !state.error, text: state.text, reasoning: state.reasoning || undefined, error: state.error, stopReason: state.stopReason, sessionId: state.sessionId, usage: usageOf() });
165
+ if (ev.type !== 'result')
166
+ continue;
167
+ const hasError = !!ev.is_error || (Array.isArray(ev.errors) && ev.errors.length > 0) || !!state.error;
168
+ const pending = pendingClaudeBackgroundTasks(state);
169
+ if (decideClaudeResultSettle({ hasError, pendingBackground: pending }) === 'hold') {
170
+ // Background work is still running: do NOT end the turn here. Keep stdin open so Claude
171
+ // wakes itself up and streams a follow-up turn reporting the finished work — we settle
172
+ // on THAT result (pending back to 0). A cap bounds the wait so a never-completing daemon
173
+ // (e.g. a dev server) eventually settles (graceful, no kill) instead of holding forever.
174
+ if (!holdCapTimer) {
175
+ holdCapTimer = setTimeout(() => { if (!settled)
176
+ settleResult({ stopReason: 'background', kill: false }); }, claudeBgHoldCapMs());
177
+ if (typeof holdCapTimer.unref === 'function')
178
+ holdCapTimer.unref();
179
+ }
180
+ continue;
125
181
  }
182
+ settleResult();
126
183
  }
127
184
  });
128
185
  child.stderr.on('data', (c) => { stderr += c.toString('utf8'); });
129
186
  child.on('error', (err) => finish({ ok: false, text: state.text, error: `claude spawn error: ${err.message}`, stopReason: 'error' }));
130
187
  child.on('close', (code) => {
188
+ if (settled)
189
+ return;
131
190
  if (ctx.signal.aborted) {
132
- finish({ ok: false, text: state.text, reasoning: state.reasoning, error: 'Interrupted by user.', stopReason: 'interrupted', sessionId: state.sessionId, usage: usageOf() });
191
+ finish({ ok: false, text: state.text, reasoning: state.reasoning, error: 'Interrupted by user.', stopReason: 'interrupted', sessionId: state.sessionId, usage: usageOf() }, false);
133
192
  return;
134
193
  }
135
194
  const ok = !state.error && code === 0;
136
- finish({ ok, text: state.text, reasoning: state.reasoning || undefined, error: state.error || (ok ? null : `claude exited ${code}${stderr ? `: ${stderr.slice(0, 300)}` : ''}`), stopReason: state.stopReason, sessionId: state.sessionId, usage: usageOf() });
195
+ finish({ ok, text: state.text, reasoning: state.reasoning || undefined, error: state.error || (ok ? null : `claude exited ${code}${stderr ? `: ${stderr.slice(0, 300)}` : ''}`), stopReason: state.stopReason, sessionId: state.sessionId, usage: usageOf() }, false);
137
196
  });
138
197
  try {
139
- if (steerable)
140
- child.stdin.write(claudeUserMessage(input.prompt) + '\n'); // keep stdin open for steering
141
- else {
142
- child.stdin.write(input.prompt);
143
- child.stdin.end();
144
- }
198
+ // Send the prompt as a stream-json user message and keep stdin OPEN (do not end it here):
199
+ // closing it makes Claude exit at the first `result`, before any background task finishes.
200
+ child.stdin.write(claudeUserMessage(input.prompt, input.attachments) + '\n');
145
201
  }
146
202
  catch { /* ignore */ }
147
203
  });
@@ -215,6 +271,7 @@ export function handleClaudeEvent(ev, s, emit) {
215
271
  return;
216
272
  }
217
273
  if (t === 'system') {
274
+ trackClaudeBackgroundTask(ev, s);
218
275
  if (ev.session_id && ev.session_id !== s.sessionId) {
219
276
  s.sessionId = ev.session_id;
220
277
  emit({ type: 'session', sessionId: ev.session_id });
@@ -301,6 +358,34 @@ export function handleClaudeEvent(ev, s, emit) {
301
358
  emit({ type: 'plan', plan });
302
359
  continue;
303
360
  }
361
+ // Task list (current Claude mechanism): stash the subject; the tool_result assigns the id.
362
+ // Plan-only — like the legacy driver these never surface as Activity rows.
363
+ if (name === 'TaskCreate') {
364
+ const subject = typeof b.input?.subject === 'string' ? b.input.subject.trim() : '';
365
+ if (subject)
366
+ (s.pendingTaskCreates ||= new Map()).set(id, { subject });
367
+ continue;
368
+ }
369
+ if (name === 'TaskUpdate') {
370
+ const taskId = String(b.input?.taskId ?? '').trim();
371
+ const rawStatus = String(b.input?.status ?? '').trim().toLowerCase();
372
+ if (taskId) {
373
+ if (rawStatus === 'deleted') {
374
+ s.taskList?.delete(taskId);
375
+ if (Array.isArray(s.taskOrder))
376
+ s.taskOrder = s.taskOrder.filter((x) => x !== taskId);
377
+ }
378
+ else if (rawStatus) {
379
+ const existing = s.taskList?.get(taskId);
380
+ if (existing)
381
+ existing.status = rawStatus;
382
+ }
383
+ const plan = rebuildClaudeTaskPlan(s);
384
+ if (plan)
385
+ emit({ type: 'plan', plan });
386
+ }
387
+ continue;
388
+ }
304
389
  if (name === 'Task' || name === 'Agent') {
305
390
  const input = b.input || {};
306
391
  const sub = {
@@ -348,6 +433,23 @@ export function handleClaudeEvent(ev, s, emit) {
348
433
  continue;
349
434
  emitClaudeImages(b.content || [], s, emit);
350
435
  const id = String(b.tool_use_id || '').trim();
436
+ // TaskCreate result: the assigned task id arrives here; register the task and emit the plan.
437
+ if (id && s.pendingTaskCreates?.has(id)) {
438
+ const pending = s.pendingTaskCreates.get(id);
439
+ const assignedId = readClaudeTaskCreateId(ev, b);
440
+ if (pending && assignedId) {
441
+ s.pendingTaskCreates.delete(id);
442
+ (s.taskList ||= new Map());
443
+ (s.taskOrder ||= []);
444
+ if (!s.taskList.has(assignedId))
445
+ s.taskOrder.push(assignedId);
446
+ s.taskList.set(assignedId, { subject: pending.subject, status: 'pending' });
447
+ const plan = rebuildClaudeTaskPlan(s);
448
+ if (plan)
449
+ emit({ type: 'plan', plan });
450
+ }
451
+ continue;
452
+ }
351
453
  const tool = id ? s.tools?.get(id) : undefined;
352
454
  if (!tool)
353
455
  continue;
@@ -384,10 +486,86 @@ export function handleClaudeEvent(ev, s, emit) {
384
486
  return;
385
487
  }
386
488
  }
387
- // A stream-json user message (for --input-format stream-json; used to send the prompt and
388
- // to inject mid-turn steer messages while stdin stays open).
389
- export function claudeUserMessage(text) {
390
- return JSON.stringify({ type: 'user', message: { role: 'user', content: [{ type: 'text', text }] } });
489
+ // ── run_in_background lifecycle (background wake-up) ───────────────────────────────────
490
+ // Claude streams a structured lifecycle for detached background work as `system` sub-events:
491
+ // { subtype:'task_started', task_id, tool_use_id, description } ← launched
492
+ // { subtype:'task_updated', task_id, patch:{ status } } progress / terminal
493
+ // { subtype:'task_notification', task_id, status } ← terminal (completed/killed/…)
494
+ // A task counts as pending from task_started until a terminal task_updated/task_notification.
495
+ // While any are pending the driver keeps the turn alive instead of ending it at `result`, so
496
+ // Claude's own background→wake-up turn (which reports the finished work) can stream in. Pure +
497
+ // exported for hermetic testing.
498
+ // How long, with no settle, to keep holding a turn open for a background task to finish and
499
+ // trigger Claude's wake-up turn, before giving up (so a never-completing daemon doesn't hang
500
+ // the turn forever). Override with PIKILOOM_CLAUDE_BG_HOLD_MS.
501
+ const CLAUDE_BG_HOLD_CAP_DEFAULT_MS = 10 * 60_000;
502
+ export function claudeBgHoldCapMs() {
503
+ const raw = Number(process.env.PIKILOOM_CLAUDE_BG_HOLD_MS);
504
+ return Number.isFinite(raw) && raw > 0 ? raw : CLAUDE_BG_HOLD_CAP_DEFAULT_MS;
505
+ }
506
+ // After we settle a held turn gracefully (stdin closed, no kill), force-kill the lingering
507
+ // process only if it hasn't exited on its own within this window. Backstop against leaks.
508
+ const CLAUDE_EXIT_LEAK_GUARD_MS = 15_000;
509
+ export function isTerminalTaskStatus(status) {
510
+ return /^(complete|done|success|succeed|finish|kill|fail|error|stop|cancel|abort|timed?_?out|timeout)/i
511
+ .test(String(status ?? '').trim());
512
+ }
513
+ export function trackClaudeBackgroundTask(ev, s) {
514
+ const subtype = ev?.subtype;
515
+ if (subtype !== 'task_started' && subtype !== 'task_updated' && subtype !== 'task_notification')
516
+ return;
517
+ const id = String(ev?.task_id ?? ev?.tool_use_id ?? '').trim();
518
+ if (!id)
519
+ return;
520
+ if (subtype === 'task_started') {
521
+ (s.bgStarted ||= new Set()).add(id);
522
+ return;
523
+ }
524
+ if (isTerminalTaskStatus(ev?.patch?.status ?? ev?.status))
525
+ (s.bgTerminal ||= new Set()).add(id);
526
+ }
527
+ export function pendingClaudeBackgroundTasks(s) {
528
+ const started = s?.bgStarted;
529
+ if (!started?.size)
530
+ return 0;
531
+ const terminal = s?.bgTerminal;
532
+ let n = 0;
533
+ for (const id of started)
534
+ if (!terminal?.has(id))
535
+ n++;
536
+ return n;
537
+ }
538
+ // At a `result` event: settle the turn normally, unless background work is still running
539
+ // (then hold the process open for the wake-up). Errors always settle.
540
+ export function decideClaudeResultSettle(input) {
541
+ if (input.hasError)
542
+ return 'settle';
543
+ return input.pendingBackground > 0 ? 'hold' : 'settle';
544
+ }
545
+ // Claude vision input formats (matches the legacy driver / Anthropic API: png, jpeg, gif, webp).
546
+ const CLAUDE_IMAGE_MIME = {
547
+ '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
548
+ '.gif': 'image/gif', '.webp': 'image/webp',
549
+ };
550
+ // A stream-json user message (for --input-format stream-json; used to send the prompt and to
551
+ // inject mid-turn steer messages while stdin stays open). Image attachments are inlined as base64
552
+ // image content blocks so the model actually sees them; other files become a text note. Without
553
+ // this the kernel path sent text only and silently dropped pasted/attached images.
554
+ export function claudeUserMessage(text, attachments) {
555
+ const content = [];
556
+ for (const filePath of attachments || []) {
557
+ const mime = CLAUDE_IMAGE_MIME[extname(filePath).toLowerCase()];
558
+ if (mime) {
559
+ try {
560
+ content.push({ type: 'image', source: { type: 'base64', media_type: mime, data: readFileSync(filePath).toString('base64') } });
561
+ continue;
562
+ }
563
+ catch { /* unreadable -> fall through to a text note */ }
564
+ }
565
+ content.push({ type: 'text', text: `[Attached file: ${filePath}]` });
566
+ }
567
+ content.push({ type: 'text', text });
568
+ return JSON.stringify({ type: 'user', message: { role: 'user', content } });
391
569
  }
392
570
  // Surface base64 image content blocks as artifacts (data URLs), deduped per turn.
393
571
  export function emitClaudeImages(blocks, s, emit) {
@@ -425,6 +603,45 @@ export function todoWriteToPlan(input) {
425
603
  }
426
604
  return steps.length ? { explanation: null, steps } : null;
427
605
  }
606
+ // ── Claude task-list (TaskCreate / TaskUpdate) -> UniversalPlan ──────────────────────
607
+ // Current Claude Code drives its task list through TaskCreate/TaskUpdate, NOT TodoWrite
608
+ // (TodoWrite is the legacy mechanism). TaskCreate carries the subject; its tool_result then
609
+ // assigns a stable task id (toolUseResult.task.id, or "Task #N" in the text). TaskUpdate flips
610
+ // a task's status by id. We accumulate the list in driver state and re-emit the whole plan on
611
+ // each change. Without this the kernel path never emits a plan event for Claude, so the
612
+ // dashboard's task-list card never renders. Ported from pikiloom's legacy claude driver.
613
+ // The assigned task id from a TaskCreate tool_result. Prefer the structured field; fall back
614
+ // to parsing "Task #N" from a string result.
615
+ export function readClaudeTaskCreateId(ev, block) {
616
+ const structured = ev?.toolUseResult?.task?.id;
617
+ if (structured != null && String(structured).trim())
618
+ return String(structured).trim();
619
+ const content = block?.content;
620
+ if (typeof content === 'string') {
621
+ const m = content.match(/Task #(\d+)/);
622
+ if (m)
623
+ return m[1];
624
+ }
625
+ return null;
626
+ }
627
+ export function rebuildClaudeTaskPlan(s) {
628
+ if (!Array.isArray(s?.taskOrder) || !s.taskOrder.length)
629
+ return null;
630
+ const steps = [];
631
+ for (const id of s.taskOrder) {
632
+ const task = s.taskList?.get(id);
633
+ if (!task)
634
+ continue;
635
+ const lowered = String(task.status || '').toLowerCase();
636
+ const status = lowered === 'completed' ? 'completed'
637
+ : (lowered === 'in_progress' || lowered === 'inprogress') ? 'inProgress'
638
+ : 'pending';
639
+ const text = String(task.subject || '').trim();
640
+ if (text)
641
+ steps.push({ text, status });
642
+ }
643
+ return steps.length ? { explanation: null, steps } : null;
644
+ }
428
645
  // ── Tool-call summarization (ported from pikiloom's summarizeClaudeToolUse) ──────────
429
646
  // Turns a Claude tool_use {name,input} into a one-line human summary. The runtime's
430
647
  // activity projector joins these into snapshot.activity; the structured form lives in
@@ -241,7 +241,21 @@ export class CodexDriver {
241
241
  const ok = await srv.start();
242
242
  if (!ok)
243
243
  return { ok: false, text: '', error: 'failed to start codex app-server', stopReason: 'error' };
244
- const onAbort = () => srv.kill();
244
+ let settle = () => { };
245
+ const turnDone = new Promise((res) => { settle = res; });
246
+ // On abort: gracefully interrupt the running turn, then settle turnDone OURSELVES. A bare
247
+ // srv.kill() (SIGTERM) never produces a turn/completed notification, so without this explicit
248
+ // settle() the `await turnDone` below hangs forever — run() never resolves and the task stays
249
+ // "running" in the orchestrator even though the codex process is already dead ("停止不掉,但实际上已经停了").
250
+ const onAbort = () => {
251
+ if (state.sessionId && state.turnId) {
252
+ srv.call('turn/interrupt', { threadId: state.sessionId, turnId: state.turnId }, 5_000).finally(() => settle());
253
+ }
254
+ else {
255
+ srv.kill();
256
+ settle();
257
+ }
258
+ };
245
259
  if (ctx.signal.aborted)
246
260
  onAbort();
247
261
  else
@@ -264,8 +278,6 @@ export class CodexDriver {
264
278
  state.sessionId = threadId;
265
279
  ctx.emit({ type: 'session', sessionId: threadId });
266
280
  }
267
- let settle = () => { };
268
- const turnDone = new Promise((res) => { settle = res; });
269
281
  srv.onNotification((method, params) => {
270
282
  if (params?.threadId && params.threadId !== state.sessionId && method !== 'turn/started')
271
283
  return;
package/dist/index.d.ts CHANGED
@@ -9,7 +9,7 @@ export type { SessionStore, CoreSessionRecord, ModelResolver, ModelInjection, To
9
9
  export type { LoomIO, PromptInput, Surface, SurfaceCapabilities, Plugin, SpawnContribution, } from './contracts/surface.js';
10
10
  export { FsSessionStore, NullModelResolver, NoopToolProvider, PassthroughSystemPromptBuilder, AutoCancelInteractionHandler, DeferToTerminalInteractionHandler, NoopCatalog, defaultBaseDir, } from './ports/defaults.js';
11
11
  export { EchoDriver } from './drivers/echo.js';
12
- export { ClaudeDriver } from './drivers/claude.js';
12
+ export { ClaudeDriver, isTerminalTaskStatus, trackClaudeBackgroundTask, pendingClaudeBackgroundTasks, decideClaudeResultSettle, claudeBgHoldCapMs, type ClaudeResultSettleDecision, } from './drivers/claude.js';
13
13
  export { CodexDriver } from './drivers/codex.js';
14
14
  export { GeminiDriver } from './drivers/gemini.js';
15
15
  export { AcpDriver, type AcpDriverConfig } from './drivers/acp.js';
package/dist/index.js CHANGED
@@ -19,7 +19,7 @@ export { attachTui } from './runtime/tui.js';
19
19
  export { FsSessionStore, NullModelResolver, NoopToolProvider, PassthroughSystemPromptBuilder, AutoCancelInteractionHandler, DeferToTerminalInteractionHandler, NoopCatalog, defaultBaseDir, } from './ports/defaults.js';
20
20
  // Drivers & surfaces (also available via subpath exports)
21
21
  export { EchoDriver } from './drivers/echo.js';
22
- export { ClaudeDriver } from './drivers/claude.js';
22
+ export { ClaudeDriver, isTerminalTaskStatus, trackClaudeBackgroundTask, pendingClaudeBackgroundTasks, decideClaudeResultSettle, claudeBgHoldCapMs, } from './drivers/claude.js';
23
23
  export { CodexDriver } from './drivers/codex.js';
24
24
  export { GeminiDriver } from './drivers/gemini.js';
25
25
  export { AcpDriver } from './drivers/acp.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikiloom/kernel",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Heterogeneous coding agents (Claude / Codex / Gemini / ACP) -> an interaction-friendly, accumulating session snapshot + control handle, over pluggable surfaces (IM, Web, tunnel, TUI). The reusable core pikiloom itself is built on.",
5
5
  "type": "module",
6
6
  "license": "MIT",