@pikiloom/kernel 0.1.4 → 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.
- package/README.md +35 -0
- package/dist/contracts/driver.d.ts +15 -0
- package/dist/contracts/ports.d.ts +2 -0
- package/dist/drivers/claude.d.ts +5 -1
- package/dist/drivers/claude.js +21 -0
- package/dist/drivers/codex.d.ts +22 -1
- package/dist/drivers/codex.js +87 -7
- package/dist/drivers/gemini.d.ts +5 -1
- package/dist/drivers/gemini.js +4 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -0
- package/dist/ports/defaults.js +7 -1
- package/dist/runtime/loom.d.ts +10 -0
- package/dist/runtime/loom.js +18 -2
- package/dist/workspace/index.d.ts +5 -0
- package/dist/workspace/index.js +7 -0
- package/dist/workspace/mcp.d.ts +44 -0
- package/dist/workspace/mcp.js +82 -0
- package/dist/workspace/native.d.ts +14 -0
- package/dist/workspace/native.js +340 -0
- package/dist/workspace/paths.d.ts +33 -0
- package/dist/workspace/paths.js +40 -0
- package/dist/workspace/sessions.d.ts +49 -0
- package/dist/workspace/sessions.js +130 -0
- package/dist/workspace/skills.d.ts +44 -0
- package/dist/workspace/skills.js +173 -0
- package/llms.txt +23 -1
- package/package.json +1 -1
|
@@ -0,0 +1,130 @@
|
|
|
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
|
+
}
|
|
6
|
+
function ts(iso) {
|
|
7
|
+
if (!iso)
|
|
8
|
+
return 0;
|
|
9
|
+
const n = Date.parse(iso);
|
|
10
|
+
return Number.isNaN(n) ? 0 : n;
|
|
11
|
+
}
|
|
12
|
+
function newer(a, b) {
|
|
13
|
+
return ts(b) > ts(a) ? b : a;
|
|
14
|
+
}
|
|
15
|
+
function managedToInfo(agent, rec) {
|
|
16
|
+
return {
|
|
17
|
+
sessionKey: `${agent}:${rec.sessionId}`,
|
|
18
|
+
agent,
|
|
19
|
+
sessionId: rec.sessionId,
|
|
20
|
+
title: rec.title ?? null,
|
|
21
|
+
preview: rec.preview ?? null,
|
|
22
|
+
workdir: rec.workdir ?? null,
|
|
23
|
+
model: rec.model ?? null,
|
|
24
|
+
effort: rec.effort ?? null,
|
|
25
|
+
runState: rec.runState ?? null,
|
|
26
|
+
running: rec.runState === 'running',
|
|
27
|
+
source: 'managed',
|
|
28
|
+
createdAt: rec.createdAt ?? null,
|
|
29
|
+
updatedAt: rec.updatedAt ?? null,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function nativeToInfo(agent, n) {
|
|
33
|
+
return {
|
|
34
|
+
sessionKey: `${agent}:${n.sessionId}`,
|
|
35
|
+
agent,
|
|
36
|
+
sessionId: n.sessionId,
|
|
37
|
+
title: n.title,
|
|
38
|
+
preview: n.preview,
|
|
39
|
+
workdir: n.cwd,
|
|
40
|
+
model: n.model,
|
|
41
|
+
effort: null,
|
|
42
|
+
runState: n.running ? 'running' : 'completed',
|
|
43
|
+
running: n.running,
|
|
44
|
+
source: 'native',
|
|
45
|
+
createdAt: n.createdAt,
|
|
46
|
+
updatedAt: n.updatedAt,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export class SessionsManager {
|
|
50
|
+
deps;
|
|
51
|
+
constructor(deps) {
|
|
52
|
+
this.deps = deps;
|
|
53
|
+
}
|
|
54
|
+
async list(opts = {}) {
|
|
55
|
+
const scope = opts.scope ?? 'all';
|
|
56
|
+
const workdir = path.resolve(opts.workdir || this.deps.defaultWorkdir);
|
|
57
|
+
const includeNative = opts.includeNative !== false;
|
|
58
|
+
const drivers = this.deps.drivers();
|
|
59
|
+
const agentIds = opts.agent ? [opts.agent] : [...drivers.keys()];
|
|
60
|
+
const byKey = new Map();
|
|
61
|
+
// Managed sessions (the kernel's own store).
|
|
62
|
+
for (const agent of agentIds) {
|
|
63
|
+
let records = [];
|
|
64
|
+
try {
|
|
65
|
+
records = await this.deps.store.list(agent);
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
this.deps.log?.(`[sessions] store.list(${agent}) failed: ${e?.message || e}`);
|
|
69
|
+
}
|
|
70
|
+
for (const rec of records) {
|
|
71
|
+
if (scope === 'workspace') {
|
|
72
|
+
if (!rec.workdir || path.resolve(rec.workdir) !== workdir)
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
byKey.set(`${agent}:${rec.sessionId}`, managedToInfo(agent, rec));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Native sessions (the agents' own stores) — inherently per-workdir.
|
|
79
|
+
if (includeNative) {
|
|
80
|
+
for (const agent of agentIds) {
|
|
81
|
+
const driver = drivers.get(agent);
|
|
82
|
+
if (!driver?.listNativeSessions)
|
|
83
|
+
continue;
|
|
84
|
+
let natives = [];
|
|
85
|
+
try {
|
|
86
|
+
natives = await driver.listNativeSessions({ workdir, limit: opts.limit });
|
|
87
|
+
}
|
|
88
|
+
catch (e) {
|
|
89
|
+
this.deps.log?.(`[sessions] ${agent}.listNativeSessions failed: ${e?.message || e}`);
|
|
90
|
+
}
|
|
91
|
+
for (const n of natives) {
|
|
92
|
+
const key = `${agent}:${n.sessionId}`;
|
|
93
|
+
const existing = byKey.get(key);
|
|
94
|
+
if (existing) {
|
|
95
|
+
// Same identity discovered both ways: managed record wins, but adopt the newer
|
|
96
|
+
// timestamp and backfill any fields the managed record never captured.
|
|
97
|
+
existing.updatedAt = newer(existing.updatedAt, n.updatedAt);
|
|
98
|
+
existing.preview = existing.preview ?? n.preview;
|
|
99
|
+
existing.title = existing.title ?? n.title;
|
|
100
|
+
existing.model = existing.model ?? n.model;
|
|
101
|
+
existing.running = existing.running || n.running;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
byKey.set(key, nativeToInfo(agent, n));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const out = [...byKey.values()].sort((a, b) => ts(b.updatedAt) - ts(a.updatedAt));
|
|
110
|
+
return typeof opts.limit === 'number' ? out.slice(0, Math.max(0, opts.limit)) : out;
|
|
111
|
+
}
|
|
112
|
+
async search(opts) {
|
|
113
|
+
const q = (opts.query || '').trim().toLowerCase();
|
|
114
|
+
const all = await this.list({ ...opts, limit: undefined });
|
|
115
|
+
const matched = q
|
|
116
|
+
? all.filter(s => [s.title, s.preview, s.sessionId, s.model, s.agent].some(v => v != null && v.toLowerCase().includes(q)))
|
|
117
|
+
: all;
|
|
118
|
+
return typeof opts.limit === 'number' ? matched.slice(0, Math.max(0, opts.limit)) : matched;
|
|
119
|
+
}
|
|
120
|
+
async get(sessionKey, opts = {}) {
|
|
121
|
+
const { agent, sessionId } = splitKey(sessionKey);
|
|
122
|
+
if (!agent)
|
|
123
|
+
return null;
|
|
124
|
+
const rec = await this.deps.store.get(agent, sessionId).catch(() => null);
|
|
125
|
+
if (rec)
|
|
126
|
+
return managedToInfo(agent, rec);
|
|
127
|
+
const list = await this.list({ agent, workdir: opts.workdir });
|
|
128
|
+
return list.find(s => s.sessionKey === sessionKey) ?? null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { LoomPaths, LoomScope } from './paths.js';
|
|
2
|
+
export interface SkillInfo {
|
|
3
|
+
name: string;
|
|
4
|
+
label: string | null;
|
|
5
|
+
description: string | null;
|
|
6
|
+
scope: LoomScope;
|
|
7
|
+
path: string;
|
|
8
|
+
mcpRequires?: string[];
|
|
9
|
+
}
|
|
10
|
+
export interface SkillSearchResult {
|
|
11
|
+
name: string;
|
|
12
|
+
description: string | null;
|
|
13
|
+
source: string;
|
|
14
|
+
author?: string | null;
|
|
15
|
+
homepage?: string | null;
|
|
16
|
+
version?: string | null;
|
|
17
|
+
}
|
|
18
|
+
export interface SkillsManagerOptions {
|
|
19
|
+
paths: LoomPaths;
|
|
20
|
+
/** Agent skills dirs (relative to the scope's agentHome) to symlink to the canonical dir. */
|
|
21
|
+
agentSkillDirs?: string[];
|
|
22
|
+
log?: (msg: string) => void;
|
|
23
|
+
}
|
|
24
|
+
/** Make `linkPath` a symlink to `targetDir` (idempotent; replaces a stale link/dir). */
|
|
25
|
+
export declare function ensureDirSymlink(linkPath: string, targetDir: string): void;
|
|
26
|
+
export declare class SkillsManager {
|
|
27
|
+
private readonly paths;
|
|
28
|
+
private readonly agentSkillDirs;
|
|
29
|
+
private readonly log?;
|
|
30
|
+
constructor(opts: SkillsManagerOptions);
|
|
31
|
+
/** The canonical skills dir for a scope, e.g. <workdir>/.pikiloom/skills or ~/.pikiloom/skills. */
|
|
32
|
+
canonicalDir(scope: LoomScope, workdir?: string): string;
|
|
33
|
+
/** Absolute paths of the per-agent skill dirs that link to the canonical dir for a scope. */
|
|
34
|
+
agentLinkPaths(scope: LoomScope, workdir?: string): string[];
|
|
35
|
+
/** Ensure the canonical dir exists and each agent skills dir symlinks to it. */
|
|
36
|
+
ensureLinks(scope: LoomScope, workdir?: string): void;
|
|
37
|
+
/** List skills. scope 'all' (default) = workspace (if workdir) then global, project wins on name clash. */
|
|
38
|
+
list(opts?: {
|
|
39
|
+
workdir?: string;
|
|
40
|
+
scope?: LoomScope | 'all';
|
|
41
|
+
}): SkillInfo[];
|
|
42
|
+
/** Search installable skills on the npm registry (best-effort; [] on failure). */
|
|
43
|
+
search(query: string, limit?: number): Promise<SkillSearchResult[]>;
|
|
44
|
+
}
|
|
@@ -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
|
|
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",
|