@pikiloom/kernel 0.2.20 → 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.
@@ -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.20",
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",