pikiloom 0.4.61 → 0.4.63
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 +1 -1
- package/dist/browser-profile.js +3 -2
- package/dist/core/constants.js +4 -0
- package/package.json +2 -1
- package/packages/kernel/README.md +20 -7
- package/packages/kernel/dist/contracts/driver.d.ts +1 -0
- package/packages/kernel/dist/drivers/acp.js +30 -163
- package/packages/kernel/dist/drivers/claude.d.ts +2 -21
- package/packages/kernel/dist/drivers/claude.js +61 -57
- package/packages/kernel/dist/drivers/codex.d.ts +0 -10
- package/packages/kernel/dist/drivers/codex.js +47 -144
- package/packages/kernel/dist/drivers/gemini.js +9 -27
- package/packages/kernel/dist/drivers/hermes.d.ts +1 -2
- package/packages/kernel/dist/drivers/hermes.js +1 -4
- package/packages/kernel/dist/drivers/index.d.ts +5 -4
- package/packages/kernel/dist/drivers/index.js +8 -4
- package/packages/kernel/dist/{workspace → drivers}/native.d.ts +0 -1
- package/packages/kernel/dist/{workspace → drivers}/native.js +212 -45
- package/packages/kernel/dist/drivers/rpc.d.ts +45 -0
- package/packages/kernel/dist/drivers/rpc.js +130 -0
- package/packages/kernel/dist/drivers/shared.d.ts +21 -0
- package/packages/kernel/dist/drivers/shared.js +66 -0
- package/packages/kernel/dist/index.d.ts +2 -1
- package/packages/kernel/dist/index.js +10 -2
- package/packages/kernel/dist/protocol/index.d.ts +5 -0
- package/packages/kernel/dist/protocol/index.js +10 -0
- package/packages/kernel/dist/runtime/hub.js +10 -8
- package/packages/kernel/dist/workspace/index.d.ts +1 -2
- package/packages/kernel/dist/workspace/index.js +3 -2
- package/packages/kernel/dist/workspace/mcp.js +6 -16
- package/packages/kernel/dist/workspace/npm-search.d.ts +9 -0
- package/packages/kernel/dist/workspace/npm-search.js +21 -0
- package/packages/kernel/dist/workspace/sessions.d.ts +3 -0
- package/packages/kernel/dist/workspace/sessions.js +28 -14
- package/packages/kernel/dist/workspace/skills.d.ts +11 -0
- package/packages/kernel/dist/workspace/skills.js +14 -20
|
@@ -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
|
|
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:
|
|
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 ?
|
|
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 =
|
|
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);
|
|
@@ -124,6 +120,12 @@ export class Hub {
|
|
|
124
120
|
extraArgs: spawn.extraArgs,
|
|
125
121
|
configOverrides: spawn.configOverrides,
|
|
126
122
|
extraMcpServers: tools.servers,
|
|
123
|
+
// The managed path's control verb `LoomIO.steer()` can only reach a driver that was
|
|
124
|
+
// launched steer-enabled, so honor the driver's declared capability here — otherwise
|
|
125
|
+
// steer is a silent no-op for steer-capable agents (claude gates registerSteer +
|
|
126
|
+
// --replay-user-messages on this flag). Drivers that don't support steer stay
|
|
127
|
+
// un-steerable; those that steer over their own channel regardless (codex RPC) ignore it.
|
|
128
|
+
steerable: !!driver.capabilities?.steer,
|
|
127
129
|
};
|
|
128
130
|
runner.run(driver, turnInput, input.prompt, model, effort)
|
|
129
131
|
.then(async (result) => {
|
|
@@ -320,7 +322,7 @@ export class Hub {
|
|
|
320
322
|
return e ? { snapshot: e.snapshot, seq: e.seq } : null;
|
|
321
323
|
}
|
|
322
324
|
async getHistory(sessionKey) {
|
|
323
|
-
const { agent, sessionId } =
|
|
325
|
+
const { agent, sessionId } = splitSessionKey(sessionKey);
|
|
324
326
|
if (!agent || !this.deps.sessionStore.history)
|
|
325
327
|
return [];
|
|
326
328
|
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
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
+
}
|
|
@@ -16,6 +16,7 @@ export interface ManagedSessionInfo {
|
|
|
16
16
|
source: SessionSource;
|
|
17
17
|
createdAt: string | null;
|
|
18
18
|
updatedAt: string | null;
|
|
19
|
+
messageCount?: number | null;
|
|
19
20
|
}
|
|
20
21
|
export interface ListSessionsOptions {
|
|
21
22
|
/**
|
|
@@ -28,6 +29,8 @@ export interface ListSessionsOptions {
|
|
|
28
29
|
agent?: string;
|
|
29
30
|
includeNative?: boolean;
|
|
30
31
|
limit?: number;
|
|
32
|
+
/** Skip this many of the most-recent rows before taking `limit` — for paginated "load more". */
|
|
33
|
+
offset?: number;
|
|
31
34
|
}
|
|
32
35
|
export interface SearchSessionsOptions extends ListSessionsOptions {
|
|
33
36
|
query: string;
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
|
|
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:
|
|
14
|
+
sessionKey: makeSessionKey(agent, rec.sessionId),
|
|
18
15
|
agent,
|
|
19
16
|
sessionId: rec.sessionId,
|
|
20
17
|
title: rec.title ?? null,
|
|
@@ -27,23 +24,25 @@ function managedToInfo(agent, rec) {
|
|
|
27
24
|
source: 'managed',
|
|
28
25
|
createdAt: rec.createdAt ?? null,
|
|
29
26
|
updatedAt: rec.updatedAt ?? null,
|
|
27
|
+
messageCount: null, // managed records don't track a turn count; native rows carry the head-approx one
|
|
30
28
|
};
|
|
31
29
|
}
|
|
32
30
|
function nativeToInfo(agent, n) {
|
|
33
31
|
return {
|
|
34
|
-
sessionKey:
|
|
32
|
+
sessionKey: makeSessionKey(agent, n.sessionId),
|
|
35
33
|
agent,
|
|
36
34
|
sessionId: n.sessionId,
|
|
37
35
|
title: n.title,
|
|
38
36
|
preview: n.preview,
|
|
39
37
|
workdir: n.cwd,
|
|
40
38
|
model: n.model,
|
|
41
|
-
effort: null,
|
|
39
|
+
effort: n.effort ?? null,
|
|
42
40
|
runState: n.running ? 'running' : 'completed',
|
|
43
41
|
running: n.running,
|
|
44
42
|
source: 'native',
|
|
45
43
|
createdAt: n.createdAt,
|
|
46
44
|
updatedAt: n.updatedAt,
|
|
45
|
+
messageCount: n.messageCount ?? null,
|
|
47
46
|
};
|
|
48
47
|
}
|
|
49
48
|
export class SessionsManager {
|
|
@@ -58,6 +57,15 @@ export class SessionsManager {
|
|
|
58
57
|
const drivers = this.deps.drivers();
|
|
59
58
|
const agentIds = opts.agent ? [opts.agent] : [...drivers.keys()];
|
|
60
59
|
const byKey = new Map();
|
|
60
|
+
// Native discovery must surface enough of the newest rows to satisfy offset+limit paging, since
|
|
61
|
+
// the final page is sliced AFTER merging managed + native. `undefined` limit = discover all.
|
|
62
|
+
const offset = Math.max(0, opts.offset ?? 0);
|
|
63
|
+
const nativeLimit = typeof opts.limit === 'number' ? offset + opts.limit : undefined;
|
|
64
|
+
// Native session keys a managed record already represents because the agent minted a DIVERGENT
|
|
65
|
+
// transcript id (rec.sessionId ≠ rec.nativeSessionId — the new-chat case). Those native rows are
|
|
66
|
+
// dropped below so they don't double up with their managed row. Deduping HERE (before paging) keeps
|
|
67
|
+
// page sizes and "has more" correct — doing it in a host wrapper after slicing shrinks pages.
|
|
68
|
+
const coveredNativeKeys = new Set();
|
|
61
69
|
// Managed sessions (the kernel's own store).
|
|
62
70
|
for (const agent of agentIds) {
|
|
63
71
|
let records = [];
|
|
@@ -68,11 +76,14 @@ export class SessionsManager {
|
|
|
68
76
|
this.deps.log?.(`[sessions] store.list(${agent}) failed: ${e?.message || e}`);
|
|
69
77
|
}
|
|
70
78
|
for (const rec of records) {
|
|
79
|
+
if (rec.nativeSessionId && rec.nativeSessionId !== rec.sessionId) {
|
|
80
|
+
coveredNativeKeys.add(makeSessionKey(agent, rec.nativeSessionId));
|
|
81
|
+
}
|
|
71
82
|
if (scope === 'workspace') {
|
|
72
83
|
if (!rec.workdir || path.resolve(rec.workdir) !== workdir)
|
|
73
84
|
continue;
|
|
74
85
|
}
|
|
75
|
-
byKey.set(
|
|
86
|
+
byKey.set(makeSessionKey(agent, rec.sessionId), managedToInfo(agent, rec));
|
|
76
87
|
}
|
|
77
88
|
}
|
|
78
89
|
// Native sessions (the agents' own stores) — inherently per-workdir.
|
|
@@ -83,13 +94,15 @@ export class SessionsManager {
|
|
|
83
94
|
continue;
|
|
84
95
|
let natives = [];
|
|
85
96
|
try {
|
|
86
|
-
natives = await driver.listNativeSessions({ workdir, limit:
|
|
97
|
+
natives = await driver.listNativeSessions({ workdir, limit: nativeLimit });
|
|
87
98
|
}
|
|
88
99
|
catch (e) {
|
|
89
100
|
this.deps.log?.(`[sessions] ${agent}.listNativeSessions failed: ${e?.message || e}`);
|
|
90
101
|
}
|
|
91
102
|
for (const n of natives) {
|
|
92
|
-
const key =
|
|
103
|
+
const key = makeSessionKey(agent, n.sessionId);
|
|
104
|
+
if (coveredNativeKeys.has(key))
|
|
105
|
+
continue; // a managed record already represents this transcript
|
|
93
106
|
const existing = byKey.get(key);
|
|
94
107
|
if (existing) {
|
|
95
108
|
// Same identity discovered both ways: managed record wins, but adopt the newer
|
|
@@ -107,18 +120,19 @@ export class SessionsManager {
|
|
|
107
120
|
}
|
|
108
121
|
}
|
|
109
122
|
const out = [...byKey.values()].sort((a, b) => ts(b.updatedAt) - ts(a.updatedAt));
|
|
110
|
-
return typeof opts.limit === 'number' ? out.slice(
|
|
123
|
+
return typeof opts.limit === 'number' ? out.slice(offset, offset + opts.limit) : offset ? out.slice(offset) : out;
|
|
111
124
|
}
|
|
112
125
|
async search(opts) {
|
|
113
126
|
const q = (opts.query || '').trim().toLowerCase();
|
|
114
|
-
const all = await this.list({ ...opts, limit: undefined });
|
|
127
|
+
const all = await this.list({ ...opts, limit: undefined, offset: 0 });
|
|
115
128
|
const matched = q
|
|
116
129
|
? all.filter(s => [s.title, s.preview, s.sessionId, s.model, s.agent].some(v => v != null && v.toLowerCase().includes(q)))
|
|
117
130
|
: all;
|
|
118
|
-
|
|
131
|
+
const offset = Math.max(0, opts.offset ?? 0);
|
|
132
|
+
return typeof opts.limit === 'number' ? matched.slice(offset, offset + opts.limit) : offset ? matched.slice(offset) : matched;
|
|
119
133
|
}
|
|
120
134
|
async get(sessionKey, opts = {}) {
|
|
121
|
-
const { agent, sessionId } =
|
|
135
|
+
const { agent, sessionId } = splitSessionKey(sessionKey);
|
|
122
136
|
if (!agent)
|
|
123
137
|
return null;
|
|
124
138
|
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
|
-
|
|
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
|
-
|
|
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
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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}`);
|