@pikiloom/kernel 0.2.21 → 0.3.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.
@@ -0,0 +1,66 @@
1
+ import { extname } from 'node:path';
2
+ // Driver-internal helpers shared by the concrete drivers (claude/codex/gemini/acp).
3
+ // NOT part of the public API — nothing here is re-exported by any barrel. Each helper
4
+ // exists because the same code appeared verbatim in 3+ drivers.
5
+ /** Stateful newline splitter for a child process' stdout: feed chunks, get complete lines. */
6
+ export function createLineBuffer() {
7
+ let buf = '';
8
+ return (chunk) => {
9
+ buf += typeof chunk === 'string' ? chunk : chunk.toString('utf8');
10
+ const lines = buf.split('\n');
11
+ buf = lines.pop() || '';
12
+ return lines;
13
+ };
14
+ }
15
+ /** Parse one ndjson line; undefined for blank lines / non-JSON noise. */
16
+ export function parseJsonLine(line) {
17
+ const trimmed = line.trim();
18
+ if (!trimmed)
19
+ return undefined;
20
+ try {
21
+ return JSON.parse(trimmed);
22
+ }
23
+ catch {
24
+ return undefined;
25
+ }
26
+ }
27
+ /**
28
+ * Run `fn` when the signal aborts (immediately if it already has). Returns an
29
+ * unsubscribe for drivers that must detach the handler when the turn settles.
30
+ */
31
+ export function wireAbort(signal, fn) {
32
+ if (signal.aborted) {
33
+ fn();
34
+ return () => { };
35
+ }
36
+ signal.addEventListener('abort', fn, { once: true });
37
+ return () => signal.removeEventListener('abort', fn);
38
+ }
39
+ /** SIGTERM a child, swallowing the already-dead race. */
40
+ export function sigterm(proc) {
41
+ try {
42
+ proc?.kill('SIGTERM');
43
+ }
44
+ catch { /* ignore */ }
45
+ }
46
+ // Attachment vocabulary: every driver inlines the same image formats (the Anthropic
47
+ // vision set, which the others accept too) and notes non-image files the same way.
48
+ const IMAGE_MIME_BY_EXT = {
49
+ '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
50
+ '.gif': 'image/gif', '.webp': 'image/webp',
51
+ };
52
+ /** Mime type when the file is an inlineable image, else null. */
53
+ export function imageMimeForFile(filePath) {
54
+ return IMAGE_MIME_BY_EXT[extname(filePath).toLowerCase()] ?? null;
55
+ }
56
+ /** The text note substituted for a non-image attachment. */
57
+ export function attachedFileNote(filePath) {
58
+ return `[Attached file: ${filePath}]`;
59
+ }
60
+ /**
61
+ * Context-window occupancy as a display percent (one decimal, capped at 99.9).
62
+ * Pass `used` as null when the caller wants "no data" rather than 0%.
63
+ */
64
+ export function contextPercent(used, window) {
65
+ return window && used != null ? Math.min(99.9, Math.round((used / window) * 1000) / 10) : null;
66
+ }
package/dist/index.d.ts CHANGED
@@ -9,13 +9,14 @@ 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, isTerminalTaskStatus, trackClaudeBackgroundTask, pendingClaudeBackgroundTasks, markClaudeTaskNotificationTerminal, decideClaudeResultSettle, claudeBgHoldCapMs, claudeBgAgentHoldCapMs, claudeTurnHasAgentBackground, claudeBgHoldRecheckMs, claudeBgSettleQuietMs, type ClaudeResultSettleDecision, claudeModelStallMs, claudeUserEventHasToolResult, handleClaudeEvent, claudeTurnEndedDangling, claudeTruncatedRecoveryEnabled, CLAUDE_TRUNCATED_RECOVERY_PROMPT, isClaudeSyntheticResumeNoise, claudeProducedRealOutput, claudeResumeNoopRetryLimit, } from './drivers/claude.js';
12
+ export { ClaudeDriver } 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';
16
16
  export { HermesDriver } from './drivers/hermes.js';
17
17
  export { WebSurface, type WebSurfaceOptions } from './surfaces/web.js';
18
18
  export { CliSurface } from './surfaces/cli.js';
19
+ export { discoverClaudeNativeSessions, discoverCodexNativeSessions, discoverGeminiNativeSessions, encodeClaudeProjectDir, type DiscoverOptions, } from './drivers/native.js';
19
20
  export * from './workspace/index.js';
20
21
  export * from './accounts.js';
21
22
  export * from './protocol/index.js';
package/dist/index.js CHANGED
@@ -8,6 +8,10 @@
8
8
  // await loom.start()
9
9
  //
10
10
  // pikiloom itself is just a consumer of this package.
11
+ //
12
+ // This barrel IS the public API (pinned by test/api-surface.test.ts). Driver-internal
13
+ // parser/settle helpers are exported by their modules for white-box tests but are
14
+ // deliberately NOT re-exported here.
11
15
  export { createLoom } from './runtime/loom.js';
12
16
  export { Hub } from './runtime/hub.js';
13
17
  export { SessionRunner } from './runtime/session-runner.js';
@@ -19,16 +23,20 @@ export { attachTui } from './runtime/tui.js';
19
23
  export { FsSessionStore, NullModelResolver, NoopToolProvider, PassthroughSystemPromptBuilder, AutoCancelInteractionHandler, DeferToTerminalInteractionHandler, NoopCatalog, defaultBaseDir, } from './ports/defaults.js';
20
24
  // Drivers & surfaces (also available via subpath exports)
21
25
  export { EchoDriver } from './drivers/echo.js';
22
- export { ClaudeDriver, isTerminalTaskStatus, trackClaudeBackgroundTask, pendingClaudeBackgroundTasks, markClaudeTaskNotificationTerminal, decideClaudeResultSettle, claudeBgHoldCapMs, claudeBgAgentHoldCapMs, claudeTurnHasAgentBackground, claudeBgHoldRecheckMs, claudeBgSettleQuietMs, claudeModelStallMs, claudeUserEventHasToolResult, handleClaudeEvent, claudeTurnEndedDangling, claudeTruncatedRecoveryEnabled, CLAUDE_TRUNCATED_RECOVERY_PROMPT, isClaudeSyntheticResumeNoise, claudeProducedRealOutput, claudeResumeNoopRetryLimit, } from './drivers/claude.js';
26
+ export { ClaudeDriver } from './drivers/claude.js';
23
27
  export { CodexDriver } from './drivers/codex.js';
24
28
  export { GeminiDriver } from './drivers/gemini.js';
25
29
  export { AcpDriver } from './drivers/acp.js';
26
30
  export { HermesDriver } from './drivers/hermes.js';
27
31
  export { WebSurface } from './surfaces/web.js';
28
32
  export { CliSurface } from './surfaces/cli.js';
33
+ // Native-session discovery: pure readers of each agent CLI's own transcript store
34
+ // (driver-axis knowledge; drivers implement listNativeSessions with these).
35
+ export { discoverClaudeNativeSessions, discoverCodexNativeSessions, discoverGeminiNativeSessions, encodeClaudeProjectDir, } from './drivers/native.js';
29
36
  // Workspace: unified top-level directory + session/skill/mcp management (loom.paths/sessions/skills/mcp)
30
37
  export * from './workspace/index.js';
31
- // Multi-account: per-account isolated config dirs (CLAUDE_CONFIG_DIR / CODEX_HOME)
38
+ // Multi-account: which env var carries an agent's auth token, so an app can inject a
39
+ // selected account's token per spawn (see accounts.ts for the design notes).
32
40
  export * from './accounts.js';
33
41
  // Protocol (the wire vocabulary; shared with transports)
34
42
  export * from './protocol/index.js';
@@ -101,6 +101,11 @@ export interface SessionMeta {
101
101
  phase?: SessionPhase;
102
102
  updatedAt?: number;
103
103
  }
104
+ export declare function makeSessionKey(agent: string, sessionId: string): string;
105
+ export declare function splitSessionKey(sessionKey: string): {
106
+ agent: string;
107
+ sessionId: string;
108
+ };
104
109
  export interface SnapshotPatch {
105
110
  full?: UniversalSnapshot;
106
111
  appendText?: string;
@@ -2,6 +2,16 @@
2
2
  // This is the SSOT shape: a driver-agnostic, accumulating snapshot of one session,
3
3
  // plus a small set of control verbs. Ported from pikiloom's pikichannel protocol.
4
4
  export const PROTOCOL_VERSION = 1;
5
+ // ---- Session keys ----
6
+ // A sessionKey is `${agent}:${sessionId}` — the composite id shared by the runtime, every
7
+ // store, and every terminal. These two helpers ARE that contract; nothing else parses it.
8
+ export function makeSessionKey(agent, sessionId) {
9
+ return `${agent}:${sessionId}`;
10
+ }
11
+ export function splitSessionKey(sessionKey) {
12
+ const i = sessionKey.indexOf(':');
13
+ return i < 0 ? { agent: '', sessionId: sessionKey } : { agent: sessionKey.slice(0, i), sessionId: sessionKey.slice(i + 1) };
14
+ }
5
15
  export function emptySnapshot() {
6
16
  return { phase: 'idle', updatedAt: 0 };
7
17
  }
@@ -1,10 +1,6 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { diffSnapshot, emptySnapshot, } from '../protocol/index.js';
2
+ import { diffSnapshot, emptySnapshot, makeSessionKey, splitSessionKey, } from '../protocol/index.js';
3
3
  import { SessionRunner } from './session-runner.js';
4
- function splitKey(sessionKey) {
5
- const i = sessionKey.indexOf(':');
6
- return i < 0 ? { agent: '', sessionId: sessionKey } : { agent: sessionKey.slice(0, i), sessionId: sessionKey.slice(i + 1) };
7
- }
8
4
  export class Hub {
9
5
  deps;
10
6
  sessions = new Map();
@@ -57,12 +53,12 @@ export class Hub {
57
53
  if (!driver)
58
54
  throw new Error(`No driver registered for agent "${agent}"`);
59
55
  const workdir = input.workdir || this.deps.workdir;
60
- const resumeId = input.sessionKey ? splitKey(input.sessionKey).sessionId : null;
56
+ const resumeId = input.sessionKey ? splitSessionKey(input.sessionKey).sessionId : null;
61
57
  const preExisted = resumeId ? !!(await this.deps.sessionStore.get(agent, resumeId)) : false;
62
58
  const { sessionId, workspacePath } = await this.deps.sessionStore.ensure(agent, {
63
59
  sessionId: resumeId, workdir, title: input.prompt.slice(0, 80),
64
60
  });
65
- const sessionKey = `${agent}:${sessionId}`;
61
+ const sessionKey = makeSessionKey(agent, sessionId);
66
62
  const taskId = randomUUID();
67
63
  const runner = new SessionRunner(sessionKey, agent, taskId, (snap, seq) => this.onRunnerUpdate(sessionKey, snap, seq), this.deps.interactionHandler);
68
64
  this.runnersByTask.set(taskId, runner);
@@ -320,7 +316,7 @@ export class Hub {
320
316
  return e ? { snapshot: e.snapshot, seq: e.seq } : null;
321
317
  }
322
318
  async getHistory(sessionKey) {
323
- const { agent, sessionId } = splitKey(sessionKey);
319
+ const { agent, sessionId } = splitSessionKey(sessionKey);
324
320
  if (!agent || !this.deps.sessionStore.history)
325
321
  return [];
326
322
  try {
@@ -1,5 +1,4 @@
1
1
  export { resolveLoomPaths, normalizeStateDirName, type LoomPaths, type LoomScope } from './paths.js';
2
- export { discoverClaudeNativeSessions, discoverCodexNativeSessions, discoverGeminiNativeSessions, encodeClaudeProjectDir, NATIVE_SESSION_RUNNING_THRESHOLD_MS, type DiscoverOptions, type NativeSessionInfo, } from './native.js';
3
2
  export { SessionsManager, type ManagedSessionInfo, type SessionSource, type ListSessionsOptions, type SearchSessionsOptions, type SessionsManagerDeps, } from './sessions.js';
4
- export { SkillsManager, ensureDirSymlink, type SkillInfo, type SkillSearchResult, type SkillsManagerOptions, } from './skills.js';
3
+ export { SkillsManager, ensureDirSymlink, parseSkillMeta, type SkillInfo, type SkillMeta, type SkillSearchResult, type SkillsManagerOptions, } from './skills.js';
5
4
  export { McpRegistry, type McpCatalogEntry, type McpSearchResult, type McpRegistryOptions, } from './mcp.js';
@@ -1,7 +1,8 @@
1
1
  // Workspace subsystem: the unified top-level directory + session/skill/mcp management that
2
2
  // a consuming app gets "for free" off createLoom() (exposed as loom.paths/sessions/skills/mcp).
3
+ // (Native-session discovery lives with the drivers — drivers/native.ts — because knowing each
4
+ // agent CLI's on-disk transcript format is driver-axis knowledge.)
3
5
  export { resolveLoomPaths, normalizeStateDirName } from './paths.js';
4
- export { discoverClaudeNativeSessions, discoverCodexNativeSessions, discoverGeminiNativeSessions, encodeClaudeProjectDir, NATIVE_SESSION_RUNNING_THRESHOLD_MS, } from './native.js';
5
6
  export { SessionsManager, } from './sessions.js';
6
- export { SkillsManager, ensureDirSymlink, } from './skills.js';
7
+ export { SkillsManager, ensureDirSymlink, parseSkillMeta, } from './skills.js';
7
8
  export { McpRegistry, } from './mcp.js';
@@ -1,3 +1,4 @@
1
+ import { searchNpmPackages } from './npm-search.js';
1
2
  const BUILTIN_RECOMMENDED = [
2
3
  { id: 'filesystem', name: 'Filesystem', description: 'Read/write files under allowed directories.', category: 'core', transport: { type: 'stdio', command: 'npx', args: ['-y', '@modelcontextprotocol/server-filesystem'] }, homepage: 'https://github.com/modelcontextprotocol/servers' },
3
4
  { id: 'github', name: 'GitHub', description: 'GitHub repos, issues, and PRs.', category: 'dev', brand: 'github', transport: { type: 'stdio', command: 'npx', args: ['-y', '@modelcontextprotocol/server-github'] }, envKeys: ['GITHUB_PERSONAL_ACCESS_TOKEN'], homepage: 'https://github.com/modelcontextprotocol/servers' },
@@ -57,22 +58,11 @@ export class McpRegistry {
57
58
  }
58
59
  // 2) npm fallback.
59
60
  try {
60
- const url = `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(`mcp server ${q}`.trim())}&size=${n}`;
61
- const res = await fetchImpl(url, { headers: { accept: 'application/json' } });
62
- if (!res.ok)
63
- return [];
64
- const data = await res.json();
65
- const objects = Array.isArray(data?.objects) ? data.objects : [];
66
- return objects.map((o) => {
67
- const pkg = o?.package ?? {};
68
- return {
69
- name: String(pkg.name ?? ''),
70
- description: pkg.description ?? null,
71
- source: 'npm',
72
- npmPackage: pkg.name ?? null,
73
- homepage: pkg.links?.homepage ?? pkg.links?.npm ?? null,
74
- };
75
- }).filter(s => s.name).slice(0, n);
61
+ const hits = await searchNpmPackages(`mcp server ${q}`.trim(), n, fetchImpl);
62
+ return hits.map(h => ({
63
+ name: h.name, description: h.description, source: 'npm',
64
+ npmPackage: h.name, homepage: h.homepage,
65
+ })).slice(0, n);
76
66
  }
77
67
  catch (e) {
78
68
  this.log?.(`[mcp] npm search failed: ${e?.message || e}`);
@@ -0,0 +1,9 @@
1
+ export interface NpmPackageHit {
2
+ name: string;
3
+ description: string | null;
4
+ homepage: string | null;
5
+ author: string | null;
6
+ version: string | null;
7
+ }
8
+ /** Query registry.npmjs.org's search endpoint. [] on non-OK; throws on network failure. */
9
+ export declare function searchNpmPackages(text: string, size: number, fetchImpl: typeof fetch): Promise<NpmPackageHit[]>;
@@ -0,0 +1,21 @@
1
+ // Shared npm-registry search used by SkillsManager.search and McpRegistry's npm fallback.
2
+ // Workspace-internal — not exported by any barrel.
3
+ /** Query registry.npmjs.org's search endpoint. [] on non-OK; throws on network failure. */
4
+ export async function searchNpmPackages(text, size, fetchImpl) {
5
+ const url = `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(text)}&size=${size}`;
6
+ const res = await fetchImpl(url, { headers: { accept: 'application/json' } });
7
+ if (!res.ok)
8
+ return [];
9
+ const data = await res.json();
10
+ const objects = Array.isArray(data?.objects) ? data.objects : [];
11
+ return objects.map((o) => {
12
+ const pkg = o?.package ?? {};
13
+ return {
14
+ name: String(pkg.name ?? ''),
15
+ description: pkg.description ?? null,
16
+ homepage: pkg.links?.homepage ?? pkg.links?.npm ?? null,
17
+ author: pkg.publisher?.username ?? pkg.author?.name ?? null,
18
+ version: pkg.version ?? null,
19
+ };
20
+ }).filter(h => h.name);
21
+ }
@@ -1,8 +1,5 @@
1
1
  import path from 'node:path';
2
- function splitKey(sessionKey) {
3
- const i = sessionKey.indexOf(':');
4
- return i < 0 ? { agent: '', sessionId: sessionKey } : { agent: sessionKey.slice(0, i), sessionId: sessionKey.slice(i + 1) };
5
- }
2
+ import { makeSessionKey, splitSessionKey } from '../protocol/index.js';
6
3
  function ts(iso) {
7
4
  if (!iso)
8
5
  return 0;
@@ -14,7 +11,7 @@ function newer(a, b) {
14
11
  }
15
12
  function managedToInfo(agent, rec) {
16
13
  return {
17
- sessionKey: `${agent}:${rec.sessionId}`,
14
+ sessionKey: makeSessionKey(agent, rec.sessionId),
18
15
  agent,
19
16
  sessionId: rec.sessionId,
20
17
  title: rec.title ?? null,
@@ -31,7 +28,7 @@ function managedToInfo(agent, rec) {
31
28
  }
32
29
  function nativeToInfo(agent, n) {
33
30
  return {
34
- sessionKey: `${agent}:${n.sessionId}`,
31
+ sessionKey: makeSessionKey(agent, n.sessionId),
35
32
  agent,
36
33
  sessionId: n.sessionId,
37
34
  title: n.title,
@@ -72,7 +69,7 @@ export class SessionsManager {
72
69
  if (!rec.workdir || path.resolve(rec.workdir) !== workdir)
73
70
  continue;
74
71
  }
75
- byKey.set(`${agent}:${rec.sessionId}`, managedToInfo(agent, rec));
72
+ byKey.set(makeSessionKey(agent, rec.sessionId), managedToInfo(agent, rec));
76
73
  }
77
74
  }
78
75
  // Native sessions (the agents' own stores) — inherently per-workdir.
@@ -89,7 +86,7 @@ export class SessionsManager {
89
86
  this.deps.log?.(`[sessions] ${agent}.listNativeSessions failed: ${e?.message || e}`);
90
87
  }
91
88
  for (const n of natives) {
92
- const key = `${agent}:${n.sessionId}`;
89
+ const key = makeSessionKey(agent, n.sessionId);
93
90
  const existing = byKey.get(key);
94
91
  if (existing) {
95
92
  // Same identity discovered both ways: managed record wins, but adopt the newer
@@ -118,7 +115,7 @@ export class SessionsManager {
118
115
  return typeof opts.limit === 'number' ? matched.slice(0, Math.max(0, opts.limit)) : matched;
119
116
  }
120
117
  async get(sessionKey, opts = {}) {
121
- const { agent, sessionId } = splitKey(sessionKey);
118
+ const { agent, sessionId } = splitSessionKey(sessionKey);
122
119
  if (!agent)
123
120
  return null;
124
121
  const rec = await this.deps.store.get(agent, sessionId).catch(() => null);
@@ -21,6 +21,17 @@ export interface SkillsManagerOptions {
21
21
  agentSkillDirs?: string[];
22
22
  log?: (msg: string) => void;
23
23
  }
24
+ export interface SkillMeta {
25
+ label: string | null;
26
+ description: string | null;
27
+ mcpRequires?: string[];
28
+ }
29
+ /**
30
+ * Parse a SKILL.md's frontmatter (label/name + description + mcp_requires), falling back
31
+ * to the first `# heading` for the label. Exported so an app scanning additional skill
32
+ * stores (e.g. an agent's own ~/.claude/skills) reads them with the same rules.
33
+ */
34
+ export declare function parseSkillMeta(content: string): SkillMeta;
24
35
  /** Make `linkPath` a symlink to `targetDir` (idempotent; replaces a stale link/dir). */
25
36
  export declare function ensureDirSymlink(linkPath: string, targetDir: string): void;
26
37
  export declare class SkillsManager {
@@ -1,13 +1,20 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { searchNpmPackages } from './npm-search.js';
3
4
  const DEFAULT_AGENT_SKILL_DIRS = ['.claude/skills', '.agents/skills'];
4
- function parseSkillMeta(content) {
5
+ /**
6
+ * Parse a SKILL.md's frontmatter (label/name + description + mcp_requires), falling back
7
+ * to the first `# heading` for the label. Exported so an app scanning additional skill
8
+ * stores (e.g. an agent's own ~/.claude/skills) reads them with the same rules.
9
+ */
10
+ export function parseSkillMeta(content) {
5
11
  let label = null;
6
12
  let description = null;
7
13
  let mcpRequires;
8
14
  const fm = content.match(/^---\s*\n([\s\S]*?)\n---/);
9
15
  if (fm) {
10
- const lm = fm[1].match(/^label:\s*(.+)/m);
16
+ // Both frontmatter dialects: pikiloom's `label:` and the agent-native `name:`.
17
+ const lm = fm[1].match(/^label:\s*(.+)/m) || fm[1].match(/^name:\s*(.+)/m);
11
18
  if (lm)
12
19
  label = lm[1].trim();
13
20
  const dm = fm[1].match(/^description:\s*(.+)/m);
@@ -145,25 +152,12 @@ export class SkillsManager {
145
152
  /** Search installable skills on the npm registry (best-effort; [] on failure). */
146
153
  async search(query, limit = 20) {
147
154
  const q = (query || '').trim();
148
- const text = encodeURIComponent(`agent skill ${q}`.trim());
149
- const url = `https://registry.npmjs.org/-/v1/search?text=${text}&size=${Math.max(1, Math.min(50, limit))}`;
150
155
  try {
151
- const res = await fetch(url, { headers: { accept: 'application/json' } });
152
- if (!res.ok)
153
- return [];
154
- const data = await res.json();
155
- const objects = Array.isArray(data?.objects) ? data.objects : [];
156
- return objects.map((o) => {
157
- const pkg = o?.package ?? {};
158
- return {
159
- name: String(pkg.name ?? ''),
160
- description: pkg.description ?? null,
161
- source: 'npm',
162
- author: pkg.publisher?.username ?? pkg.author?.name ?? null,
163
- homepage: pkg.links?.homepage ?? pkg.links?.npm ?? null,
164
- version: pkg.version ?? null,
165
- };
166
- }).filter(s => s.name);
156
+ const hits = await searchNpmPackages(`agent skill ${q}`.trim(), Math.max(1, Math.min(50, limit)), fetch);
157
+ return hits.map(h => ({
158
+ name: h.name, description: h.description, source: 'npm',
159
+ author: h.author, homepage: h.homepage, version: h.version,
160
+ }));
167
161
  }
168
162
  catch (e) {
169
163
  this.log?.(`[skills] search failed: ${e?.message || e}`);
package/llms.txt CHANGED
@@ -59,6 +59,7 @@ interface UniversalSnapshot {
59
59
  }
60
60
  ```
61
61
  Wire delta helpers: `diffSnapshot(prev,next) => SnapshotPatch` (prefix-append text/reasoning; undefined→null clears) and `applySnapshotPatch(prev,patch)`. `emptySnapshot()`, `PROTOCOL_VERSION`.
62
+ Session keys: a sessionKey is `${agent}:${sessionId}` — compose/parse ONLY via `makeSessionKey(agent, sessionId)` / `splitSessionKey(sessionKey)`.
62
63
 
63
64
  ## Control verbs (LoomIO, via loom.io)
64
65
 
@@ -86,7 +87,8 @@ governs a session index, a skills registry and per-agent symlinks, resolved in t
86
87
  - `loom.skills: SkillsManager` — the canonical skills registry. `canonicalDir(scope,wd?)` = `<root>/skills`;
87
88
  `ensureLinks(scope, wd?)` symlinks it into each agent's dir (default `.claude/skills`, `.agents/skills`,
88
89
  overridable via `agentSkillDirs`) so a skill registered once is visible to every agent. `list({workdir?,scope?})`,
89
- `search(query) => npm results`.
90
+ `search(query) => npm results`. `parseSkillMeta(content)` (exported) reads a SKILL.md's frontmatter
91
+ (label/name + description + mcp_requires) — reuse it when scanning additional skill stores yourself.
90
92
  - `loom.mcp: McpRegistry` — `recommended(): McpCatalogEntry[]` (curated), `search(query)` (MCP registry → npm),
91
93
  `toServerSpec(entry, env?) => McpServerSpec`. Enabling/injecting a server stays on the Plugin.tools()/ToolProvider seam.
92
94
 
@@ -95,12 +97,18 @@ Drivers expose native discovery via the optional `AgentDriver.listNativeSessions
95
97
 
96
98
  ## Drivers (pass to createLoom({drivers}) or loom.registerDriver)
97
99
 
98
- - ClaudeDriver (id 'claude'): `claude` CLI stream-json, supports --effort; steer ✓, resume ✓, tui
100
+ - ClaudeDriver (id 'claude'): `claude` CLI stream-json, supports --effort; steer ✓, resume ✓, tui ✓.
101
+ Background-hold/stall/recovery heuristics tunable via PIKILOOM_CLAUDE_* env (see README).
99
102
  - CodexDriver (id 'codex'): `codex app-server` JSON-RPC; steer ✓, HITL via askUser, resume ✓, tui ✓
100
103
  - GeminiDriver (id 'gemini'): `gemini` stream-json; resume ✓, tui ✓
101
- - HermesDriver (id 'hermes'): ACP session/update; resume ✓
104
+ - AcpDriver (id from config): generic ACP ndjson JSON-RPC — any ACP CLI via `new AcpDriver({ id, command, args })`; HITL via askUser, resume ✓
105
+ - HermesDriver (id 'hermes'): preset `new AcpDriver({ id:'hermes', command:'hermes', args:['acp'] })`; HITL via askUser, resume ✓
102
106
  - EchoDriver (id 'echo'): hermetic in-process test driver; all caps ✓
103
107
 
108
+ Multi-account: `accountTokenSupported(agent)` / `accountTokenEnvVar(agent)` / `accountTokenEnv(agent, token)` —
109
+ which env var carries an agent's auth token (claude: CLAUDE_CODE_OAUTH_TOKEN), so an app injects a selected
110
+ account's token per spawn (via ModelResolver env or a Plugin contributeSpawn). Storage/selection stay app-side.
111
+
104
112
  Write a driver: implement `AgentDriver { id; capabilities?; run(input, ctx); tui?(input) }`. In run(), call `ctx.emit(event)`:
105
113
  `DriverEvent = {type:'session',sessionId} | {type:'text',delta} | {type:'reasoning',delta} | {type:'tool',call:UniversalToolCall} | {type:'plan',plan} | {type:'subagent',subagent} | {type:'usage',usage} | {type:'artifact',artifact} | {type:'activity',line}`.
106
114
  `ctx = { signal: AbortSignal, emit, askUser(interaction)=>Promise<answers>, registerSteer(fn) }`. Return `DriverResult { ok, text, reasoning?, error?, stopReason?, sessionId?, usage? }`. Emit a `tool` event on BOTH start (status 'running') and completion (status 'done'/'failed'); the kernel projects `activity` from these.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikiloom/kernel",
3
- "version": "0.2.21",
3
+ "version": "0.3.1",
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",