@pikiloom/kernel 0.1.5 → 0.2.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,173 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ const DEFAULT_AGENT_SKILL_DIRS = ['.claude/skills', '.agents/skills'];
4
+ function parseSkillMeta(content) {
5
+ let label = null;
6
+ let description = null;
7
+ let mcpRequires;
8
+ const fm = content.match(/^---\s*\n([\s\S]*?)\n---/);
9
+ if (fm) {
10
+ const lm = fm[1].match(/^label:\s*(.+)/m);
11
+ if (lm)
12
+ label = lm[1].trim();
13
+ const dm = fm[1].match(/^description:\s*(.+)/m);
14
+ if (dm)
15
+ description = dm[1].trim();
16
+ const mr = fm[1].match(/^mcp_requires:\s*\n((?:\s+-\s+.+\n?)+)/m);
17
+ if (mr) {
18
+ mcpRequires = mr[1].split('\n').map(l => l.replace(/^\s*-\s*/, '').replace(/["']/g, '').trim()).filter(Boolean);
19
+ }
20
+ }
21
+ if (!label) {
22
+ const hm = content.match(/^#\s+(.+)$/m);
23
+ if (hm)
24
+ label = hm[1].trim();
25
+ }
26
+ return { label, description, mcpRequires };
27
+ }
28
+ function realPathOrNull(p) {
29
+ try {
30
+ return fs.realpathSync(p);
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ }
36
+ /** Make `linkPath` a symlink to `targetDir` (idempotent; replaces a stale link/dir). */
37
+ export function ensureDirSymlink(linkPath, targetDir) {
38
+ const desiredTarget = path.relative(path.dirname(linkPath), targetDir) || '.';
39
+ try {
40
+ const stat = fs.lstatSync(linkPath);
41
+ if (stat.isSymbolicLink()) {
42
+ const currentTarget = fs.readlinkSync(linkPath);
43
+ const currentReal = realPathOrNull(path.resolve(path.dirname(linkPath), currentTarget));
44
+ const desiredReal = realPathOrNull(targetDir);
45
+ if (currentTarget === desiredTarget || (currentReal && desiredReal && currentReal === desiredReal))
46
+ return;
47
+ fs.unlinkSync(linkPath);
48
+ }
49
+ else {
50
+ fs.rmSync(linkPath, { recursive: true, force: true });
51
+ }
52
+ }
53
+ catch { /* nothing there yet */ }
54
+ fs.mkdirSync(path.dirname(linkPath), { recursive: true });
55
+ try {
56
+ fs.symlinkSync(desiredTarget, linkPath, process.platform === 'win32' ? 'junction' : 'dir');
57
+ }
58
+ catch (err) {
59
+ if (err?.code !== 'EEXIST' || fs.readlinkSync(linkPath) !== desiredTarget)
60
+ throw err;
61
+ }
62
+ }
63
+ function discoverSkillsFromDir(dir, scope, seen) {
64
+ let entries;
65
+ try {
66
+ entries = fs.readdirSync(dir).sort((a, b) => a.localeCompare(b, 'en', { sensitivity: 'base' }));
67
+ }
68
+ catch {
69
+ return [];
70
+ }
71
+ const out = [];
72
+ for (const name of entries) {
73
+ if (!name || seen.has(name))
74
+ continue;
75
+ const skillDir = path.join(dir, name);
76
+ const skillFile = path.join(skillDir, 'SKILL.md');
77
+ try {
78
+ if (!fs.statSync(skillDir).isDirectory())
79
+ continue;
80
+ }
81
+ catch {
82
+ continue;
83
+ }
84
+ try {
85
+ if (!fs.statSync(skillFile).isFile())
86
+ continue;
87
+ }
88
+ catch {
89
+ continue;
90
+ }
91
+ let meta = { label: null, description: null };
92
+ try {
93
+ meta = parseSkillMeta(fs.readFileSync(skillFile, 'utf8'));
94
+ }
95
+ catch { /* keep defaults */ }
96
+ out.push({ name, label: meta.label, description: meta.description, scope, path: skillDir, mcpRequires: meta.mcpRequires });
97
+ seen.add(name);
98
+ }
99
+ return out;
100
+ }
101
+ export class SkillsManager {
102
+ paths;
103
+ agentSkillDirs;
104
+ log;
105
+ constructor(opts) {
106
+ this.paths = opts.paths;
107
+ this.agentSkillDirs = opts.agentSkillDirs?.length ? opts.agentSkillDirs : DEFAULT_AGENT_SKILL_DIRS;
108
+ this.log = opts.log;
109
+ }
110
+ /** The canonical skills dir for a scope, e.g. <workdir>/.pikiloom/skills or ~/.pikiloom/skills. */
111
+ canonicalDir(scope, workdir) {
112
+ return this.paths.skillsDir(scope, workdir);
113
+ }
114
+ /** Absolute paths of the per-agent skill dirs that link to the canonical dir for a scope. */
115
+ agentLinkPaths(scope, workdir) {
116
+ const base = this.paths.agentHome(scope, workdir);
117
+ return this.agentSkillDirs.map(rel => path.join(base, rel));
118
+ }
119
+ /** Ensure the canonical dir exists and each agent skills dir symlinks to it. */
120
+ ensureLinks(scope, workdir) {
121
+ const canonical = this.canonicalDir(scope, workdir);
122
+ fs.mkdirSync(canonical, { recursive: true });
123
+ for (const link of this.agentLinkPaths(scope, workdir)) {
124
+ try {
125
+ ensureDirSymlink(link, canonical);
126
+ }
127
+ catch (e) {
128
+ this.log?.(`[skills] link ${link} -> ${canonical} failed: ${e?.message || e}`);
129
+ }
130
+ }
131
+ }
132
+ /** List skills. scope 'all' (default) = workspace (if workdir) then global, project wins on name clash. */
133
+ list(opts = {}) {
134
+ const scope = opts.scope ?? 'all';
135
+ const seen = new Set();
136
+ const out = [];
137
+ if ((scope === 'workspace' || scope === 'all') && opts.workdir) {
138
+ out.push(...discoverSkillsFromDir(this.paths.skillsDir('workspace', opts.workdir), 'workspace', seen));
139
+ }
140
+ if (scope === 'global' || scope === 'all') {
141
+ out.push(...discoverSkillsFromDir(this.paths.skillsDir('global'), 'global', seen));
142
+ }
143
+ return out;
144
+ }
145
+ /** Search installable skills on the npm registry (best-effort; [] on failure). */
146
+ async search(query, limit = 20) {
147
+ 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
+ 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);
167
+ }
168
+ catch (e) {
169
+ this.log?.(`[skills] search failed: ${e?.message || e}`);
170
+ return [];
171
+ }
172
+ }
173
+ }
package/llms.txt CHANGED
@@ -35,7 +35,7 @@ await loom.start();
35
35
  const { sessionKey, taskId } = await loom.io.prompt({ prompt: 'hi', agent: 'claude' });
36
36
  const unsub = loom.io.subscribe((key, snapshot, patch, seq) => render(snapshot));
37
37
  ```
38
- Every port has a default (zero config). Override any: sessionStore, modelResolver, toolProvider, systemPromptBuilder, catalog, interactionHandler, plugins, serialPerSession, appNamespace, workdir, defaultAgent, log.
38
+ Every port has a default (zero config). Override any: sessionStore, modelResolver, toolProvider, systemPromptBuilder, catalog, interactionHandler, plugins, serialPerSession, stateDirName, appNamespace, agentSkillDirs, workdir, defaultAgent, log.
39
39
 
40
40
  3) RAW TUI passthrough — `loom.runTui({ agent, workdir })` (full stdio passthrough) or `loom.openTui(...) => PtyBridge` (onData/write/resize/onExit). Needs optional `node-pty`; `ptyAvailable()` reports it.
41
41
 
@@ -71,6 +71,28 @@ Wire delta helpers: `diffSnapshot(prev,next) => SnapshotPatch` (prefix-append te
71
71
  - discovery: `listModels(agent)`, `listEffort(agent, model?)`, `listTools(agent, workdir?)`, `listSkills(agent, workdir?)`
72
72
  Concurrent prompts to one session queue by default (serialPerSession=true).
73
73
 
74
+ ## Workspace: unified top-level directory + session/skill/mcp management (loom.paths/sessions/skills/mcp)
75
+
76
+ One explicitly-configurable top-level dir (`createLoom({ stateDirName })`, default `'pikiloom'` → `.pikiloom`)
77
+ governs a session index, a skills registry and per-agent symlinks, resolved in two scopes: global
78
+ (`~/.pikiloom`) and per-workspace (`<workdir>/.pikiloom`). Exposed off the Loom:
79
+
80
+ - `loom.paths: LoomPaths` — `resolveLoomPaths({stateDirName})`. `globalRoot`, `workspaceRoot(wd)`,
81
+ `sessionsDir/skillsDir/mcpConfigPath(scope, wd?)`, `agentHome(scope, wd?)`. (`scope: 'global'|'workspace'`.)
82
+ - `loom.sessions: SessionsManager` — the unified, searchable session read-model. Merges MANAGED sessions
83
+ (the SessionStore, scoped per workspace by the cwd they ran in) with each agent's NATIVE sessions
84
+ (driver.listNativeSessions — claude/codex/gemini read their own on-disk transcripts). `list({scope:'workspace'|'global'|'all', workdir?, agent?, includeNative?, limit?}) => ManagedSessionInfo[]`,
85
+ `search({query, ...}) `, `get(sessionKey)`. This is "全局 + 每个工作区的会话列表,含 claude/codex 自己的会话 + 搜索".
86
+ - `loom.skills: SkillsManager` — the canonical skills registry. `canonicalDir(scope,wd?)` = `<root>/skills`;
87
+ `ensureLinks(scope, wd?)` symlinks it into each agent's dir (default `.claude/skills`, `.agents/skills`,
88
+ overridable via `agentSkillDirs`) so a skill registered once is visible to every agent. `list({workdir?,scope?})`,
89
+ `search(query) => npm results`.
90
+ - `loom.mcp: McpRegistry` — `recommended(): McpCatalogEntry[]` (curated), `search(query)` (MCP registry → npm),
91
+ `toServerSpec(entry, env?) => McpServerSpec`. Enabling/injecting a server stays on the Plugin.tools()/ToolProvider seam.
92
+
93
+ Drivers expose native discovery via the optional `AgentDriver.listNativeSessions?({workdir, limit}) => NativeSessionInfo[]`
94
+ (implemented by Claude/Codex/Gemini; pure node-builtins, also exported as `discover{Claude,Codex,Gemini}NativeSessions`).
95
+
74
96
  ## Drivers (pass to createLoom({drivers}) or loom.registerDriver)
75
97
 
76
98
  - ClaudeDriver (id 'claude'): `claude` CLI stream-json, supports --effort; steer ✓, resume ✓, tui ✓
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikiloom/kernel",
3
- "version": "0.1.5",
3
+ "version": "0.2.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",