@jbarket/loomai 0.5.0
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/CHANGELOG.md +475 -0
- package/LICENSE +661 -0
- package/README.md +702 -0
- package/assets/demo.cast +396 -0
- package/assets/skill/SKILL.md +192 -0
- package/dist/backends/embedding-runtime.d.ts +39 -0
- package/dist/backends/embedding-runtime.js +265 -0
- package/dist/backends/episodes.d.ts +36 -0
- package/dist/backends/episodes.js +126 -0
- package/dist/backends/fastembed.d.ts +18 -0
- package/dist/backends/fastembed.js +95 -0
- package/dist/backends/glob.d.ts +10 -0
- package/dist/backends/glob.js +16 -0
- package/dist/backends/index.d.ts +6 -0
- package/dist/backends/index.js +78 -0
- package/dist/backends/migrations.d.ts +38 -0
- package/dist/backends/migrations.js +157 -0
- package/dist/backends/mmr.d.ts +45 -0
- package/dist/backends/mmr.js +82 -0
- package/dist/backends/proposals.d.ts +65 -0
- package/dist/backends/proposals.js +182 -0
- package/dist/backends/recall-log.d.ts +42 -0
- package/dist/backends/recall-log.js +143 -0
- package/dist/backends/retry.d.ts +29 -0
- package/dist/backends/retry.js +60 -0
- package/dist/backends/salience.d.ts +57 -0
- package/dist/backends/salience.js +186 -0
- package/dist/backends/sqlite-knowledge.d.ts +37 -0
- package/dist/backends/sqlite-knowledge.js +788 -0
- package/dist/backends/sqlite-vec.d.ts +64 -0
- package/dist/backends/sqlite-vec.js +737 -0
- package/dist/backends/ttl.d.ts +24 -0
- package/dist/backends/ttl.js +50 -0
- package/dist/backends/types.d.ts +562 -0
- package/dist/backends/types.js +8 -0
- package/dist/blocks/harness.d.ts +45 -0
- package/dist/blocks/harness.js +142 -0
- package/dist/blocks/model.d.ts +4 -0
- package/dist/blocks/model.js +56 -0
- package/dist/blocks/types.d.ts +40 -0
- package/dist/blocks/types.js +34 -0
- package/dist/categories.d.ts +16 -0
- package/dist/categories.js +29 -0
- package/dist/cli/args.d.ts +21 -0
- package/dist/cli/args.js +47 -0
- package/dist/cli/bootstrap.d.ts +2 -0
- package/dist/cli/bootstrap.js +214 -0
- package/dist/cli/doctor.d.ts +2 -0
- package/dist/cli/doctor.js +206 -0
- package/dist/cli/forget.d.ts +2 -0
- package/dist/cli/forget.js +97 -0
- package/dist/cli/harness.d.ts +2 -0
- package/dist/cli/harness.js +91 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +127 -0
- package/dist/cli/inject.d.ts +2 -0
- package/dist/cli/inject.js +298 -0
- package/dist/cli/install.d.ts +2 -0
- package/dist/cli/install.js +137 -0
- package/dist/cli/io.d.ts +14 -0
- package/dist/cli/io.js +63 -0
- package/dist/cli/knowledge.d.ts +2 -0
- package/dist/cli/knowledge.js +216 -0
- package/dist/cli/memory.d.ts +2 -0
- package/dist/cli/memory.js +608 -0
- package/dist/cli/migrate.d.ts +2 -0
- package/dist/cli/migrate.js +116 -0
- package/dist/cli/recall.d.ts +2 -0
- package/dist/cli/recall.js +91 -0
- package/dist/cli/remember.d.ts +2 -0
- package/dist/cli/remember.js +111 -0
- package/dist/cli/serve.d.ts +2 -0
- package/dist/cli/serve.js +71 -0
- package/dist/cli/subcommands.d.ts +7 -0
- package/dist/cli/subcommands.js +11 -0
- package/dist/cli/tui/multi-select.d.ts +53 -0
- package/dist/cli/tui/multi-select.js +116 -0
- package/dist/cli/update-identity.d.ts +2 -0
- package/dist/cli/update-identity.js +90 -0
- package/dist/cli/update.d.ts +2 -0
- package/dist/cli/update.js +87 -0
- package/dist/cli/wake.d.ts +2 -0
- package/dist/cli/wake.js +53 -0
- package/dist/clients.d.ts +8 -0
- package/dist/clients.js +63 -0
- package/dist/config.d.ts +59 -0
- package/dist/config.js +160 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +75 -0
- package/dist/injection/harnesses.d.ts +17 -0
- package/dist/injection/harnesses.js +51 -0
- package/dist/injection/render.d.ts +17 -0
- package/dist/injection/render.js +50 -0
- package/dist/injection/writer.d.ts +25 -0
- package/dist/injection/writer.js +118 -0
- package/dist/install/harnesses.d.ts +26 -0
- package/dist/install/harnesses.js +90 -0
- package/dist/install/names.d.ts +16 -0
- package/dist/install/names.js +36 -0
- package/dist/install/render.d.ts +18 -0
- package/dist/install/render.js +42 -0
- package/dist/install/skill-source.d.ts +1 -0
- package/dist/install/skill-source.js +11 -0
- package/dist/path-safety.d.ts +19 -0
- package/dist/path-safety.js +62 -0
- package/dist/server.d.ts +8 -0
- package/dist/server.js +571 -0
- package/dist/tools/append-maintenance-log.d.ts +52 -0
- package/dist/tools/append-maintenance-log.js +103 -0
- package/dist/tools/archive.d.ts +2 -0
- package/dist/tools/archive.js +21 -0
- package/dist/tools/bootstrap.d.ts +11 -0
- package/dist/tools/bootstrap.js +161 -0
- package/dist/tools/dossier.d.ts +1 -0
- package/dist/tools/dossier.js +102 -0
- package/dist/tools/eurorack-import.d.ts +47 -0
- package/dist/tools/eurorack-import.js +199 -0
- package/dist/tools/find-similar.d.ts +3 -0
- package/dist/tools/find-similar.js +26 -0
- package/dist/tools/forget.d.ts +6 -0
- package/dist/tools/forget.js +70 -0
- package/dist/tools/harness.d.ts +18 -0
- package/dist/tools/harness.js +54 -0
- package/dist/tools/identity-scaffold.d.ts +22 -0
- package/dist/tools/identity-scaffold.js +142 -0
- package/dist/tools/identity.d.ts +4 -0
- package/dist/tools/identity.js +226 -0
- package/dist/tools/knowledge-archive.d.ts +5 -0
- package/dist/tools/knowledge-archive.js +24 -0
- package/dist/tools/knowledge-history.d.ts +6 -0
- package/dist/tools/knowledge-history.js +55 -0
- package/dist/tools/knowledge-maintain.d.ts +9 -0
- package/dist/tools/knowledge-maintain.js +113 -0
- package/dist/tools/knowledge-merge.d.ts +8 -0
- package/dist/tools/knowledge-merge.js +40 -0
- package/dist/tools/knowledge-move.d.ts +10 -0
- package/dist/tools/knowledge-move.js +48 -0
- package/dist/tools/knowledge-purge.d.ts +5 -0
- package/dist/tools/knowledge-purge.js +20 -0
- package/dist/tools/knowledge-recall.d.ts +20 -0
- package/dist/tools/knowledge-recall.js +142 -0
- package/dist/tools/knowledge-restore.d.ts +4 -0
- package/dist/tools/knowledge-restore.js +24 -0
- package/dist/tools/knowledge-supersede.d.ts +6 -0
- package/dist/tools/knowledge-supersede.js +36 -0
- package/dist/tools/knowledge-verify.d.ts +8 -0
- package/dist/tools/knowledge-verify.js +39 -0
- package/dist/tools/knowledge-write.d.ts +25 -0
- package/dist/tools/knowledge-write.js +87 -0
- package/dist/tools/memory-audit.d.ts +3 -0
- package/dist/tools/memory-audit.js +47 -0
- package/dist/tools/memory-list.d.ts +4 -0
- package/dist/tools/memory-list.js +23 -0
- package/dist/tools/prune.d.ts +4 -0
- package/dist/tools/prune.js +28 -0
- package/dist/tools/recall.d.ts +4 -0
- package/dist/tools/recall.js +24 -0
- package/dist/tools/remember.d.ts +10 -0
- package/dist/tools/remember.js +43 -0
- package/dist/tools/restore.d.ts +2 -0
- package/dist/tools/restore.js +20 -0
- package/dist/tools/update-identity.d.ts +34 -0
- package/dist/tools/update-identity.js +194 -0
- package/dist/tools/update.d.ts +2 -0
- package/dist/tools/update.js +16 -0
- package/dist/transport/guards.d.ts +35 -0
- package/dist/transport/guards.js +107 -0
- package/dist/transport/http-server.d.ts +17 -0
- package/dist/transport/http-server.js +198 -0
- package/dist/transport/mcp-app-hello.d.ts +3 -0
- package/dist/transport/mcp-app-hello.js +66 -0
- package/dist/widgets/display-modes.html +131 -0
- package/dist/widgets/hello.html +138 -0
- package/dist/widgets/proposals.html +131 -0
- package/dist/widgets/top-of-mind.html +131 -0
- package/package.json +73 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type WriteAction = 'created' | 'appended' | 'updated' | 'no-change';
|
|
2
|
+
export interface WriteResult {
|
|
3
|
+
action: WriteAction;
|
|
4
|
+
path: string;
|
|
5
|
+
bytesWritten: number;
|
|
6
|
+
}
|
|
7
|
+
export type PreviewAction = WriteAction;
|
|
8
|
+
export declare class MalformedMarkersError extends Error {
|
|
9
|
+
readonly path: string;
|
|
10
|
+
readonly reason: string;
|
|
11
|
+
constructor(path: string, reason: string);
|
|
12
|
+
}
|
|
13
|
+
export interface MarkerBounds {
|
|
14
|
+
startIdx: number;
|
|
15
|
+
endTerminusIdx: number;
|
|
16
|
+
}
|
|
17
|
+
export declare function findMarkers(text: string, path: string): MarkerBounds | null;
|
|
18
|
+
export declare function normalizeLF(s: string): string;
|
|
19
|
+
export declare function atomicWrite(path: string, content: string, preserveModeFrom: string | null): Promise<number>;
|
|
20
|
+
export declare function buildContent(existing: string | null, block: string, markers: MarkerBounds | null): {
|
|
21
|
+
next: string;
|
|
22
|
+
action: WriteAction;
|
|
23
|
+
};
|
|
24
|
+
export declare function writeManagedBlock(path: string, block: string): Promise<WriteResult>;
|
|
25
|
+
export declare function previewWrite(path: string, block: string): Promise<PreviewAction>;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marker-aware file writer for `loom inject`. Reads the target file,
|
|
3
|
+
* decides between create / append / replace / no-change, writes
|
|
4
|
+
* atomically via tmp-file + rename.
|
|
5
|
+
*
|
|
6
|
+
* The managed region is bounded by two HTML comments:
|
|
7
|
+
* <!-- loom:start v1 harness=<key> -->
|
|
8
|
+
* <!-- loom:end -->
|
|
9
|
+
* Only "loom:start" / "loom:end" literals are matched; the metadata
|
|
10
|
+
* (v1, harness=...) is informational for humans and is allowed to
|
|
11
|
+
* differ between on-disk markers and the new block.
|
|
12
|
+
*/
|
|
13
|
+
import { readFile, writeFile, rename, mkdir, stat, chmod } from 'node:fs/promises';
|
|
14
|
+
import { dirname } from 'node:path';
|
|
15
|
+
export class MalformedMarkersError extends Error {
|
|
16
|
+
path;
|
|
17
|
+
reason;
|
|
18
|
+
constructor(path, reason) {
|
|
19
|
+
super(`inject: malformed loom markers in ${path}: ${reason}; fix manually or delete markers and retry`);
|
|
20
|
+
this.path = path;
|
|
21
|
+
this.reason = reason;
|
|
22
|
+
this.name = 'MalformedMarkersError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const START_RE = /<!--\s*loom:start(\s[^>]*)?-->/g;
|
|
26
|
+
const END_RE = /<!--\s*loom:end\s*-->/g;
|
|
27
|
+
export function findMarkers(text, path) {
|
|
28
|
+
const starts = [...text.matchAll(START_RE)];
|
|
29
|
+
const ends = [...text.matchAll(END_RE)];
|
|
30
|
+
if (starts.length === 0 && ends.length === 0)
|
|
31
|
+
return null;
|
|
32
|
+
if (starts.length > 1) {
|
|
33
|
+
throw new MalformedMarkersError(path, `${starts.length} start markers found, expected 1`);
|
|
34
|
+
}
|
|
35
|
+
if (ends.length > 1) {
|
|
36
|
+
throw new MalformedMarkersError(path, `${ends.length} end markers found, expected 1`);
|
|
37
|
+
}
|
|
38
|
+
if (starts.length !== ends.length) {
|
|
39
|
+
throw new MalformedMarkersError(path, `mismatched markers (start=${starts.length}, end=${ends.length})`);
|
|
40
|
+
}
|
|
41
|
+
const s = starts[0];
|
|
42
|
+
const e = ends[0];
|
|
43
|
+
const startIdx = s.index;
|
|
44
|
+
const endBegin = e.index;
|
|
45
|
+
const endTerminusIdx = endBegin + e[0].length;
|
|
46
|
+
if (endBegin < startIdx) {
|
|
47
|
+
throw new MalformedMarkersError(path, 'end marker appears before start marker');
|
|
48
|
+
}
|
|
49
|
+
return { startIdx, endTerminusIdx };
|
|
50
|
+
}
|
|
51
|
+
export function normalizeLF(s) {
|
|
52
|
+
return s.replace(/\r\n/g, '\n');
|
|
53
|
+
}
|
|
54
|
+
function ensureTrailingNewline(s) {
|
|
55
|
+
return s.endsWith('\n') ? s : s + '\n';
|
|
56
|
+
}
|
|
57
|
+
async function readIfExists(path) {
|
|
58
|
+
try {
|
|
59
|
+
return await readFile(path, 'utf-8');
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
if (err.code === 'ENOENT')
|
|
63
|
+
return null;
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export async function atomicWrite(path, content, preserveModeFrom) {
|
|
68
|
+
await mkdir(dirname(path), { recursive: true });
|
|
69
|
+
const tmp = `${path}.loom.tmp`;
|
|
70
|
+
const buf = Buffer.from(content, 'utf-8');
|
|
71
|
+
await writeFile(tmp, buf);
|
|
72
|
+
if (preserveModeFrom) {
|
|
73
|
+
try {
|
|
74
|
+
const s = await stat(preserveModeFrom);
|
|
75
|
+
await chmod(tmp, s.mode & 0o777);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
/* preserveModeFrom vanished between read and stat; leave tmp at default mode */
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
await rename(tmp, path);
|
|
82
|
+
return buf.byteLength;
|
|
83
|
+
}
|
|
84
|
+
export function buildContent(existing, block, markers) {
|
|
85
|
+
const blockLF = ensureTrailingNewline(block);
|
|
86
|
+
if (existing === null) {
|
|
87
|
+
return { next: blockLF, action: 'created' };
|
|
88
|
+
}
|
|
89
|
+
const norm = normalizeLF(existing);
|
|
90
|
+
if (markers === null) {
|
|
91
|
+
const withGap = norm.endsWith('\n') ? norm : norm + '\n';
|
|
92
|
+
const combined = ensureTrailingNewline(`${withGap}\n${blockLF}`);
|
|
93
|
+
return { next: combined, action: 'appended' };
|
|
94
|
+
}
|
|
95
|
+
const before = norm.slice(0, markers.startIdx);
|
|
96
|
+
const after = norm.slice(markers.endTerminusIdx);
|
|
97
|
+
const afterTrimmed = after.startsWith('\n') ? after.slice(1) : after;
|
|
98
|
+
const combined = ensureTrailingNewline(`${before}${blockLF.replace(/\n$/, '')}\n${afterTrimmed}`);
|
|
99
|
+
if (combined === norm) {
|
|
100
|
+
return { next: combined, action: 'no-change' };
|
|
101
|
+
}
|
|
102
|
+
return { next: combined, action: 'updated' };
|
|
103
|
+
}
|
|
104
|
+
export async function writeManagedBlock(path, block) {
|
|
105
|
+
const existing = await readIfExists(path);
|
|
106
|
+
const markers = existing !== null ? findMarkers(normalizeLF(existing), path) : null;
|
|
107
|
+
const { next, action } = buildContent(existing, block, markers);
|
|
108
|
+
if (action === 'no-change') {
|
|
109
|
+
return { action, path, bytesWritten: 0 };
|
|
110
|
+
}
|
|
111
|
+
const bytesWritten = await atomicWrite(path, next, existing !== null ? path : null);
|
|
112
|
+
return { action, path, bytesWritten };
|
|
113
|
+
}
|
|
114
|
+
export async function previewWrite(path, block) {
|
|
115
|
+
const existing = await readIfExists(path);
|
|
116
|
+
const markers = existing !== null ? findMarkers(normalizeLF(existing), path) : null;
|
|
117
|
+
return buildContent(existing, block, markers).action;
|
|
118
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type InstallTargetKey = 'claude-code' | 'codex' | 'gemini-cli' | 'opencode' | 'other';
|
|
2
|
+
export type ToolPrefix = 'mcp__loom__' | 'mcp_loom_' | 'loom_';
|
|
3
|
+
export interface InstallTarget {
|
|
4
|
+
readonly key: InstallTargetKey;
|
|
5
|
+
readonly label: string;
|
|
6
|
+
/** Canonical skill directory. `null` for `other` (caller picks a path). */
|
|
7
|
+
readonly skillDir: string | null;
|
|
8
|
+
/** Hint for the skill prose — where this harness's MCP config lives. */
|
|
9
|
+
readonly mcpConfigHint: string | null;
|
|
10
|
+
/** How the user invokes the skill inside the harness. */
|
|
11
|
+
readonly invoke: string;
|
|
12
|
+
/** Short human-readable restart instruction. */
|
|
13
|
+
readonly restart: string;
|
|
14
|
+
readonly toolPrefix: ToolPrefix;
|
|
15
|
+
}
|
|
16
|
+
export declare const SKILL_FILENAME = "loom-setup.md";
|
|
17
|
+
export declare const INSTALL_TARGETS: Readonly<Record<InstallTargetKey, InstallTarget>>;
|
|
18
|
+
export declare const INSTALL_TARGET_KEYS: readonly InstallTargetKey[];
|
|
19
|
+
export declare function isInstallTargetKey(s: string): s is InstallTargetKey;
|
|
20
|
+
export declare function getInstallTarget(key: InstallTargetKey): InstallTarget;
|
|
21
|
+
/**
|
|
22
|
+
* Resolve the skill file path for a target. Returns `null` for `other`
|
|
23
|
+
* (caller handles the dump-to-cwd branch). `home` overrides `homedir()`
|
|
24
|
+
* for tests.
|
|
25
|
+
*/
|
|
26
|
+
export declare function resolveSkillPath(t: InstallTarget, home?: string): string | null;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry of install targets for `loom install`. Separate from
|
|
3
|
+
* `src/injection/harnesses.ts` (dotfile injection paths) because the
|
|
4
|
+
* skill-install convention differs per vendor: Claude Code uses
|
|
5
|
+
* `~/.claude/skills/`, other harnesses converge on `~/.agents/skills/`.
|
|
6
|
+
*
|
|
7
|
+
* See stack spec v1 §11 (Adapters) and the alpha.6 design doc.
|
|
8
|
+
*/
|
|
9
|
+
import { homedir } from 'node:os';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
export const SKILL_FILENAME = 'loom-setup.md';
|
|
12
|
+
const HOME = homedir();
|
|
13
|
+
export const INSTALL_TARGETS = {
|
|
14
|
+
'claude-code': {
|
|
15
|
+
key: 'claude-code',
|
|
16
|
+
label: 'Claude Code',
|
|
17
|
+
skillDir: join(HOME, '.claude', 'skills'),
|
|
18
|
+
mcpConfigHint: '~/.claude.json or .mcp.json in the project root',
|
|
19
|
+
invoke: '/loom-setup',
|
|
20
|
+
restart: 'restart Claude Code (close and reopen)',
|
|
21
|
+
toolPrefix: 'mcp__loom__',
|
|
22
|
+
},
|
|
23
|
+
'codex': {
|
|
24
|
+
key: 'codex',
|
|
25
|
+
label: 'Codex',
|
|
26
|
+
skillDir: join(HOME, '.agents', 'skills'),
|
|
27
|
+
mcpConfigHint: '~/.codex/config.toml',
|
|
28
|
+
invoke: 'use the loom-setup skill',
|
|
29
|
+
restart: 'restart the Codex session',
|
|
30
|
+
toolPrefix: 'mcp_loom_',
|
|
31
|
+
},
|
|
32
|
+
'gemini-cli': {
|
|
33
|
+
key: 'gemini-cli',
|
|
34
|
+
label: 'Gemini CLI',
|
|
35
|
+
skillDir: join(HOME, '.agents', 'skills'),
|
|
36
|
+
mcpConfigHint: '~/.gemini/settings.json',
|
|
37
|
+
invoke: 'use the loom-setup skill',
|
|
38
|
+
restart: 'exit and restart Gemini CLI',
|
|
39
|
+
toolPrefix: 'mcp_loom_',
|
|
40
|
+
},
|
|
41
|
+
'opencode': {
|
|
42
|
+
key: 'opencode',
|
|
43
|
+
label: 'OpenCode',
|
|
44
|
+
skillDir: join(HOME, '.agents', 'skills'),
|
|
45
|
+
mcpConfigHint: '~/.config/opencode/config.json',
|
|
46
|
+
invoke: 'use the loom-setup skill',
|
|
47
|
+
restart: 'restart OpenCode',
|
|
48
|
+
toolPrefix: 'loom_',
|
|
49
|
+
},
|
|
50
|
+
'other': {
|
|
51
|
+
key: 'other',
|
|
52
|
+
label: 'Other (dump skill file to cwd)',
|
|
53
|
+
skillDir: null,
|
|
54
|
+
mcpConfigHint: null,
|
|
55
|
+
invoke: 'tell your agent to read loom-setup-skill.md and follow it',
|
|
56
|
+
restart: 'restart your harness after it finishes setup',
|
|
57
|
+
toolPrefix: 'mcp_loom_',
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
export const INSTALL_TARGET_KEYS = [
|
|
61
|
+
'claude-code',
|
|
62
|
+
'codex',
|
|
63
|
+
'gemini-cli',
|
|
64
|
+
'opencode',
|
|
65
|
+
'other',
|
|
66
|
+
];
|
|
67
|
+
export function isInstallTargetKey(s) {
|
|
68
|
+
return INSTALL_TARGET_KEYS.includes(s);
|
|
69
|
+
}
|
|
70
|
+
export function getInstallTarget(key) {
|
|
71
|
+
return INSTALL_TARGETS[key];
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Resolve the skill file path for a target. Returns `null` for `other`
|
|
75
|
+
* (caller handles the dump-to-cwd branch). `home` overrides `homedir()`
|
|
76
|
+
* for tests.
|
|
77
|
+
*/
|
|
78
|
+
export function resolveSkillPath(t, home) {
|
|
79
|
+
if (t.skillDir === null)
|
|
80
|
+
return null;
|
|
81
|
+
if (home === undefined)
|
|
82
|
+
return join(t.skillDir, SKILL_FILENAME);
|
|
83
|
+
switch (t.key) {
|
|
84
|
+
case 'claude-code': return join(home, '.claude', 'skills', SKILL_FILENAME);
|
|
85
|
+
case 'codex':
|
|
86
|
+
case 'gemini-cli':
|
|
87
|
+
case 'opencode': return join(home, '.agents', 'skills', SKILL_FILENAME);
|
|
88
|
+
case 'other': return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical agent-name validation. Used by `loom doctor`, `loom
|
|
3
|
+
* bootstrap` (tightening pending), and the install skill's interview.
|
|
4
|
+
* Reserved names anticipate the alpha.7+ `agents switch` pointer slot
|
|
5
|
+
* plus snapshot/export storage adjacent to agent dirs.
|
|
6
|
+
*
|
|
7
|
+
* See stack spec v1 §13 (Multi-agent layout).
|
|
8
|
+
*/
|
|
9
|
+
export declare const RESERVED_AGENT_NAMES: ReadonlySet<string>;
|
|
10
|
+
export type NameCheck = {
|
|
11
|
+
ok: true;
|
|
12
|
+
} | {
|
|
13
|
+
ok: false;
|
|
14
|
+
reason: string;
|
|
15
|
+
};
|
|
16
|
+
export declare function validateAgentName(name: string): NameCheck;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical agent-name validation. Used by `loom doctor`, `loom
|
|
3
|
+
* bootstrap` (tightening pending), and the install skill's interview.
|
|
4
|
+
* Reserved names anticipate the alpha.7+ `agents switch` pointer slot
|
|
5
|
+
* plus snapshot/export storage adjacent to agent dirs.
|
|
6
|
+
*
|
|
7
|
+
* See stack spec v1 §13 (Multi-agent layout).
|
|
8
|
+
*/
|
|
9
|
+
export const RESERVED_AGENT_NAMES = new Set([
|
|
10
|
+
'current',
|
|
11
|
+
'default',
|
|
12
|
+
'config',
|
|
13
|
+
'backups',
|
|
14
|
+
'cache',
|
|
15
|
+
'tmp',
|
|
16
|
+
'shared',
|
|
17
|
+
]);
|
|
18
|
+
const NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
19
|
+
const MAX_LEN = 64;
|
|
20
|
+
export function validateAgentName(name) {
|
|
21
|
+
if (name.length === 0)
|
|
22
|
+
return { ok: false, reason: 'Name is empty.' };
|
|
23
|
+
if (name.length > MAX_LEN) {
|
|
24
|
+
return { ok: false, reason: `Name is longer than ${MAX_LEN} characters.` };
|
|
25
|
+
}
|
|
26
|
+
if (!NAME_RE.test(name)) {
|
|
27
|
+
return {
|
|
28
|
+
ok: false,
|
|
29
|
+
reason: 'Name must be lowercase alphanumeric plus hyphens, starting with a letter or digit.',
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (RESERVED_AGENT_NAMES.has(name)) {
|
|
33
|
+
return { ok: false, reason: `"${name}" is reserved.` };
|
|
34
|
+
}
|
|
35
|
+
return { ok: true };
|
|
36
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type WriteAction = 'created' | 'skipped-exists' | 'skipped-stale' | 'overwritten';
|
|
2
|
+
export interface WriteSkillResult {
|
|
3
|
+
path: string;
|
|
4
|
+
action: WriteAction;
|
|
5
|
+
}
|
|
6
|
+
export interface WriteSkillOpts {
|
|
7
|
+
force?: boolean;
|
|
8
|
+
dryRun?: boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Write the bundled SKILL.md to `dest`. Idempotent: returns
|
|
12
|
+
* `skipped-exists` when destination already matches source,
|
|
13
|
+
* `skipped-stale` when content differs and `force` is unset (signals
|
|
14
|
+
* the on-disk copy is out of date), `overwritten` with `force: true`.
|
|
15
|
+
* `dryRun: true` short-circuits all writes but still reports the
|
|
16
|
+
* action that would have been taken.
|
|
17
|
+
*/
|
|
18
|
+
export declare function writeSkill(dest: string, opts?: WriteSkillOpts): Promise<WriteSkillResult>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { resolveSkillSourcePath } from './skill-source.js';
|
|
4
|
+
async function pathExists(p) {
|
|
5
|
+
try {
|
|
6
|
+
await readFile(p);
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Write the bundled SKILL.md to `dest`. Idempotent: returns
|
|
15
|
+
* `skipped-exists` when destination already matches source,
|
|
16
|
+
* `skipped-stale` when content differs and `force` is unset (signals
|
|
17
|
+
* the on-disk copy is out of date), `overwritten` with `force: true`.
|
|
18
|
+
* `dryRun: true` short-circuits all writes but still reports the
|
|
19
|
+
* action that would have been taken.
|
|
20
|
+
*/
|
|
21
|
+
export async function writeSkill(dest, opts = {}) {
|
|
22
|
+
const source = await readFile(resolveSkillSourcePath(), 'utf-8');
|
|
23
|
+
const exists = await pathExists(dest);
|
|
24
|
+
if (!exists) {
|
|
25
|
+
if (!opts.dryRun) {
|
|
26
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
27
|
+
await writeFile(dest, source, 'utf-8');
|
|
28
|
+
}
|
|
29
|
+
return { path: dest, action: 'created' };
|
|
30
|
+
}
|
|
31
|
+
const current = await readFile(dest, 'utf-8');
|
|
32
|
+
if (current === source) {
|
|
33
|
+
return { path: dest, action: 'skipped-exists' };
|
|
34
|
+
}
|
|
35
|
+
if (!opts.force) {
|
|
36
|
+
return { path: dest, action: 'skipped-stale' };
|
|
37
|
+
}
|
|
38
|
+
if (!opts.dryRun) {
|
|
39
|
+
await writeFile(dest, source, 'utf-8');
|
|
40
|
+
}
|
|
41
|
+
return { path: dest, action: 'overwritten' };
|
|
42
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function resolveSkillSourcePath(): string;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the bundled SKILL.md path. Works from both `src/` (dev via
|
|
3
|
+
* tsx) and `dist/` (published via `npx loom`) because
|
|
4
|
+
* `resolveRepoRoot()` walks one level up from the running module and
|
|
5
|
+
* both `src/` and `dist/` sit alongside `assets/` at the project root.
|
|
6
|
+
*/
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { resolveRepoRoot } from '../config.js';
|
|
9
|
+
export function resolveSkillSourcePath() {
|
|
10
|
+
return join(resolveRepoRoot(), 'assets', 'skill', 'SKILL.md');
|
|
11
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns a human-readable error string when `value` cannot be used as a
|
|
3
|
+
* single path segment, or null when it is safe. Rejects empty strings,
|
|
4
|
+
* '.', '..', path separators (both / and \), and null bytes.
|
|
5
|
+
*/
|
|
6
|
+
export declare function pathSegmentError(value: string, paramName: string): string | null;
|
|
7
|
+
/**
|
|
8
|
+
* Throwing variant of pathSegmentError — for block-level readers whose
|
|
9
|
+
* contract is to throw. Tool-layer functions that return error strings
|
|
10
|
+
* should call pathSegmentError directly instead.
|
|
11
|
+
*/
|
|
12
|
+
export declare function assertSafePathSegment(value: string, paramName: string): void;
|
|
13
|
+
/**
|
|
14
|
+
* Write an identity-tier file atomically (tmp + rename). When the file
|
|
15
|
+
* already exists, its prior content is preserved as `<path>.bak` first —
|
|
16
|
+
* single generation, overwriting any older .bak. First-ever writes
|
|
17
|
+
* produce no .bak.
|
|
18
|
+
*/
|
|
19
|
+
export declare function atomicWriteWithBackup(path: string, content: string): Promise<void>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path-segment validation and atomic identity writes.
|
|
3
|
+
*
|
|
4
|
+
* Boundary discipline (store-convergence spec): any param that becomes a
|
|
5
|
+
* file path segment (role, project, client, model, harness/model key)
|
|
6
|
+
* validates non-empty and separator-free at every reader, not just some
|
|
7
|
+
* writers. Without this, a value like "../../../home/user/secrets" walks
|
|
8
|
+
* out of the context directory and reads arbitrary .md files into agent
|
|
9
|
+
* context.
|
|
10
|
+
*
|
|
11
|
+
* Atomic identity writes (same spec): anything under the identity tier
|
|
12
|
+
* (IDENTITY.md, preferences.md, self-model.md) writes via tmp+rename with
|
|
13
|
+
* a single-generation `.bak` of the prior version. A crash mid-write must
|
|
14
|
+
* never truncate the creed.
|
|
15
|
+
*/
|
|
16
|
+
import { copyFile } from 'node:fs/promises';
|
|
17
|
+
import { atomicWrite } from './injection/writer.js';
|
|
18
|
+
/**
|
|
19
|
+
* Returns a human-readable error string when `value` cannot be used as a
|
|
20
|
+
* single path segment, or null when it is safe. Rejects empty strings,
|
|
21
|
+
* '.', '..', path separators (both / and \), and null bytes.
|
|
22
|
+
*/
|
|
23
|
+
export function pathSegmentError(value, paramName) {
|
|
24
|
+
if (value === '' ||
|
|
25
|
+
value === '.' ||
|
|
26
|
+
value === '..' ||
|
|
27
|
+
value.includes('/') ||
|
|
28
|
+
value.includes('\\') ||
|
|
29
|
+
value.includes('\0')) {
|
|
30
|
+
return (`Invalid ${paramName} ${JSON.stringify(value)}: ` +
|
|
31
|
+
'must be a non-empty name with no path separators.');
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Throwing variant of pathSegmentError — for block-level readers whose
|
|
37
|
+
* contract is to throw. Tool-layer functions that return error strings
|
|
38
|
+
* should call pathSegmentError directly instead.
|
|
39
|
+
*/
|
|
40
|
+
export function assertSafePathSegment(value, paramName) {
|
|
41
|
+
const error = pathSegmentError(value, paramName);
|
|
42
|
+
if (error)
|
|
43
|
+
throw new Error(error);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Write an identity-tier file atomically (tmp + rename). When the file
|
|
47
|
+
* already exists, its prior content is preserved as `<path>.bak` first —
|
|
48
|
+
* single generation, overwriting any older .bak. First-ever writes
|
|
49
|
+
* produce no .bak.
|
|
50
|
+
*/
|
|
51
|
+
export async function atomicWriteWithBackup(path, content) {
|
|
52
|
+
let existed = true;
|
|
53
|
+
try {
|
|
54
|
+
await copyFile(path, `${path}.bak`);
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
if (err.code !== 'ENOENT')
|
|
58
|
+
throw err;
|
|
59
|
+
existed = false;
|
|
60
|
+
}
|
|
61
|
+
await atomicWrite(path, content, existed ? path : null);
|
|
62
|
+
}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
export interface LoomServerConfig {
|
|
3
|
+
contextDir: string;
|
|
4
|
+
}
|
|
5
|
+
export interface LoomServerInstance {
|
|
6
|
+
server: McpServer;
|
|
7
|
+
}
|
|
8
|
+
export declare function createLoomServer(config: LoomServerConfig): LoomServerInstance;
|