harborloop 0.3.0 → 0.4.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/bin/checksums.json +12 -0
- package/bin/harbor.mjs +188 -0
- package/package.json +14 -30
- package/dist/api.d.ts +0 -109
- package/dist/api.js +0 -409
- package/dist/auth/login.d.ts +0 -14
- package/dist/auth/login.js +0 -158
- package/dist/auth/loopback.d.ts +0 -11
- package/dist/auth/loopback.js +0 -87
- package/dist/auth/pkce.d.ts +0 -7
- package/dist/auth/pkce.js +0 -14
- package/dist/auth/store.d.ts +0 -26
- package/dist/auth/store.js +0 -107
- package/dist/cli-release.d.ts +0 -31
- package/dist/cli-release.js +0 -102
- package/dist/commands/bind.d.ts +0 -23
- package/dist/commands/bind.js +0 -86
- package/dist/commands/context.d.ts +0 -12
- package/dist/commands/context.js +0 -80
- package/dist/commands/daemon.d.ts +0 -39
- package/dist/commands/daemon.js +0 -140
- package/dist/commands/doctor.d.ts +0 -16
- package/dist/commands/doctor.js +0 -937
- package/dist/commands/init.d.ts +0 -36
- package/dist/commands/init.js +0 -192
- package/dist/commands/status.d.ts +0 -5
- package/dist/commands/status.js +0 -164
- package/dist/commands/sync.d.ts +0 -40
- package/dist/commands/sync.js +0 -242
- package/dist/commands/toggle.d.ts +0 -16
- package/dist/commands/toggle.js +0 -69
- package/dist/commands/unbind.d.ts +0 -14
- package/dist/commands/unbind.js +0 -60
- package/dist/commands/workspaces.d.ts +0 -18
- package/dist/commands/workspaces.js +0 -41
- package/dist/context-budget.d.ts +0 -40
- package/dist/context-budget.js +0 -259
- package/dist/daemon/install.d.ts +0 -50
- package/dist/daemon/install.js +0 -192
- package/dist/daemon/loop.d.ts +0 -15
- package/dist/daemon/loop.js +0 -84
- package/dist/daemon/state.d.ts +0 -79
- package/dist/daemon/state.js +0 -213
- package/dist/daemon/tick.d.ts +0 -37
- package/dist/daemon/tick.js +0 -231
- package/dist/documents/apply.d.ts +0 -21
- package/dist/documents/apply.js +0 -58
- package/dist/documents/bundle.d.ts +0 -41
- package/dist/documents/bundle.js +0 -43
- package/dist/documents/inspect.d.ts +0 -12
- package/dist/documents/inspect.js +0 -33
- package/dist/documents/manifest.d.ts +0 -14
- package/dist/documents/manifest.js +0 -52
- package/dist/documents/pointer.d.ts +0 -32
- package/dist/documents/pointer.js +0 -135
- package/dist/documents/write.d.ts +0 -57
- package/dist/documents/write.js +0 -211
- package/dist/endpoints.d.ts +0 -7
- package/dist/endpoints.js +0 -15
- package/dist/env-bundle.d.ts +0 -25
- package/dist/env-bundle.js +0 -33
- package/dist/env-scan.d.ts +0 -27
- package/dist/env-scan.js +0 -41
- package/dist/env-sync.d.ts +0 -31
- package/dist/env-sync.js +0 -37
- package/dist/index.d.ts +0 -11
- package/dist/index.js +0 -282
- package/dist/install-hooks.d.ts +0 -11
- package/dist/install-hooks.js +0 -64
- package/dist/lock.d.ts +0 -31
- package/dist/lock.js +0 -192
- package/dist/machine.d.ts +0 -13
- package/dist/machine.js +0 -53
- package/dist/memories.d.ts +0 -31
- package/dist/memories.js +0 -74
- package/dist/origin.d.ts +0 -4
- package/dist/origin.js +0 -59
- package/dist/paths.d.ts +0 -12
- package/dist/paths.js +0 -45
- package/dist/providers/claude.d.ts +0 -65
- package/dist/providers/claude.js +0 -412
- package/dist/providers/codex-trust.d.ts +0 -36
- package/dist/providers/codex-trust.js +0 -141
- package/dist/providers/codex.d.ts +0 -46
- package/dist/providers/codex.js +0 -520
- package/dist/providers/cursor.d.ts +0 -45
- package/dist/providers/cursor.js +0 -344
- package/dist/providers/detect.d.ts +0 -16
- package/dist/providers/detect.js +0 -70
- package/dist/providers/hook-coverage.d.ts +0 -5
- package/dist/providers/hook-coverage.js +0 -47
- package/dist/providers/hook-entry.d.ts +0 -1
- package/dist/providers/hook-entry.js +0 -8
- package/dist/providers/surfaces.d.ts +0 -19
- package/dist/providers/surfaces.js +0 -91
- package/dist/reclaim.d.ts +0 -17
- package/dist/reclaim.js +0 -33
- package/dist/render.d.ts +0 -17
- package/dist/render.js +0 -61
- package/dist/repo-sync.d.ts +0 -27
- package/dist/repo-sync.js +0 -34
- package/dist/repos.d.ts +0 -11
- package/dist/repos.js +0 -82
- package/dist/skills.d.ts +0 -84
- package/dist/skills.js +0 -383
- package/dist/slugify.d.ts +0 -1
- package/dist/slugify.js +0 -8
- package/dist/state.d.ts +0 -23
- package/dist/state.js +0 -161
- package/dist/sync-report.d.ts +0 -59
- package/dist/sync-report.js +0 -87
- package/hooks/_recall.mjs +0 -288
- package/hooks/_receipt.mjs +0 -47
- package/hooks/_shared.mjs +0 -1610
- package/hooks/_upgrade.mjs +0 -51
- package/hooks/session-capture.mjs +0 -60
- package/hooks/session-compact.mjs +0 -19
- package/hooks/session-context.mjs +0 -258
- package/hooks/session-report.mjs +0 -42
- package/hooks/session-toolcontext.mjs +0 -183
package/dist/render.js
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
const GLYPH = {
|
|
2
|
-
ok: '✓',
|
|
3
|
-
warn: '⚠',
|
|
4
|
-
fail: '✗',
|
|
5
|
-
paused: '⏸',
|
|
6
|
-
unknown: '·',
|
|
7
|
-
};
|
|
8
|
-
function colour(state, text, enabled) {
|
|
9
|
-
if (!enabled)
|
|
10
|
-
return text;
|
|
11
|
-
const code = state === 'ok' ? 32 : state === 'warn' ? 33 : state === 'fail' ? 31 : 90;
|
|
12
|
-
return `\u001b[${code}m${text}\u001b[0m`;
|
|
13
|
-
}
|
|
14
|
-
export function renderCheck(check, opts = {}) {
|
|
15
|
-
const useColour = opts.colour ?? Boolean(process.stdout.isTTY);
|
|
16
|
-
const width = opts.labelWidth ?? 14;
|
|
17
|
-
const label = check.label.padEnd(width);
|
|
18
|
-
const glyph = colour(check.state, GLYPH[check.state], useColour);
|
|
19
|
-
const line = ` ${label} ${glyph} ${check.detail}`;
|
|
20
|
-
return check.fix ? `${line}\n ${' '.repeat(width)} ↳ ${check.fix}` : line;
|
|
21
|
-
}
|
|
22
|
-
export function renderChecks(checks, opts = {}) {
|
|
23
|
-
const labelWidth = opts.labelWidth ?? Math.max(...checks.map((c) => c.label.length), 0);
|
|
24
|
-
return checks.map((c) => renderCheck(c, { ...opts, labelWidth })).join('\n');
|
|
25
|
-
}
|
|
26
|
-
export function exitCodeFor(checks) {
|
|
27
|
-
return checks.some((c) => c.state === 'fail') ? 1 : 0;
|
|
28
|
-
}
|
|
29
|
-
export function checksToJson(checks) {
|
|
30
|
-
return checks.map((c) => ({
|
|
31
|
-
label: c.label,
|
|
32
|
-
state: c.state,
|
|
33
|
-
detail: c.detail,
|
|
34
|
-
...(c.fix ? { fix: c.fix } : {}),
|
|
35
|
-
}));
|
|
36
|
-
}
|
|
37
|
-
export function relativeTime(target, now = new Date()) {
|
|
38
|
-
const deltaMs = target.getTime() - now.getTime();
|
|
39
|
-
const future = deltaMs >= 0;
|
|
40
|
-
const abs = Math.abs(deltaMs);
|
|
41
|
-
const units = [
|
|
42
|
-
[365 * 24 * 60 * 60 * 1000, 'year'],
|
|
43
|
-
[24 * 60 * 60 * 1000, 'day'],
|
|
44
|
-
[60 * 60 * 1000, 'hour'],
|
|
45
|
-
[60 * 1000, 'minute'],
|
|
46
|
-
];
|
|
47
|
-
for (const [ms, name] of units) {
|
|
48
|
-
if (abs >= ms) {
|
|
49
|
-
const n = Math.round(abs / ms);
|
|
50
|
-
const plural = n === 1 ? name : `${name}s`;
|
|
51
|
-
return future ? `in ${n} ${plural}` : `${n} ${plural} ago`;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return 'just now';
|
|
55
|
-
}
|
|
56
|
-
export function ratio(numerator, denominator) {
|
|
57
|
-
if (denominator <= 0)
|
|
58
|
-
return `${numerator} of 0`;
|
|
59
|
-
const pct = Math.round((numerator / denominator) * 100);
|
|
60
|
-
return `${numerator} of ${denominator} (${pct}%)`;
|
|
61
|
-
}
|
package/dist/repo-sync.d.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { type ApplyDocumentsOutcome } from './documents/apply.js';
|
|
2
|
-
import { type MemorySyncSummary } from './memories.js';
|
|
3
|
-
import type { AgentPresence } from './providers/detect.js';
|
|
4
|
-
import { type SkillSyncSummary } from './skills.js';
|
|
5
|
-
export interface RepoSyncOptions {
|
|
6
|
-
readonly apiBase: string;
|
|
7
|
-
readonly workspaceId: string;
|
|
8
|
-
readonly bearerToken: string;
|
|
9
|
-
readonly repoPath: string;
|
|
10
|
-
readonly originUrl: string;
|
|
11
|
-
readonly fetchImpl?: typeof fetch;
|
|
12
|
-
readonly home?: string;
|
|
13
|
-
readonly agents?: AgentPresence;
|
|
14
|
-
}
|
|
15
|
-
export type RepoSyncResult = {
|
|
16
|
-
readonly kind: 'synced';
|
|
17
|
-
readonly documents: ApplyDocumentsOutcome;
|
|
18
|
-
readonly skills: SkillSyncSummary;
|
|
19
|
-
readonly memories: MemorySyncSummary;
|
|
20
|
-
} | {
|
|
21
|
-
readonly kind: 'sync_disabled';
|
|
22
|
-
readonly reason: 'workspace' | 'admin';
|
|
23
|
-
} | {
|
|
24
|
-
readonly kind: 'error';
|
|
25
|
-
readonly message: string;
|
|
26
|
-
};
|
|
27
|
-
export declare function syncRepoPlane(opts: RepoSyncOptions): Promise<RepoSyncResult>;
|
package/dist/repo-sync.js
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { applyDocumentsToRepo } from './documents/apply.js';
|
|
2
|
-
import { fetchContextBundle } from './documents/bundle.js';
|
|
3
|
-
import { syncMemoryFilesToHome } from './memories.js';
|
|
4
|
-
import { syncSkillFilesToRepo } from './skills.js';
|
|
5
|
-
export async function syncRepoPlane(opts) {
|
|
6
|
-
const fetched = await fetchContextBundle({
|
|
7
|
-
apiBase: opts.apiBase,
|
|
8
|
-
workspaceId: opts.workspaceId,
|
|
9
|
-
originUrl: opts.originUrl,
|
|
10
|
-
bearerToken: opts.bearerToken,
|
|
11
|
-
...(opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}),
|
|
12
|
-
});
|
|
13
|
-
if (fetched.kind === 'sync_disabled') {
|
|
14
|
-
return { kind: 'sync_disabled', reason: fetched.reason };
|
|
15
|
-
}
|
|
16
|
-
if (fetched.kind === 'error') {
|
|
17
|
-
return { kind: 'error', message: fetched.message };
|
|
18
|
-
}
|
|
19
|
-
const bundle = fetched.bundle;
|
|
20
|
-
const documents = await applyDocumentsToRepo(opts.repoPath, bundle.documents, {
|
|
21
|
-
workspaceId: opts.workspaceId,
|
|
22
|
-
});
|
|
23
|
-
const skills = await syncSkillFilesToRepo(opts.repoPath, bundle.skills, {
|
|
24
|
-
workspaceId: opts.workspaceId,
|
|
25
|
-
...(opts.home === undefined ? {} : { home: opts.home }),
|
|
26
|
-
...(opts.agents === undefined ? {} : { agents: opts.agents }),
|
|
27
|
-
});
|
|
28
|
-
const memories = await syncMemoryFilesToHome({
|
|
29
|
-
projectPath: opts.repoPath,
|
|
30
|
-
memories: bundle.memories,
|
|
31
|
-
...(opts.home === undefined ? {} : { home: opts.home }),
|
|
32
|
-
});
|
|
33
|
-
return { kind: 'synced', documents, skills, memories };
|
|
34
|
-
}
|
package/dist/repos.d.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export interface RepoEntry {
|
|
2
|
-
readonly originUrl: string;
|
|
3
|
-
readonly path: string;
|
|
4
|
-
readonly addedAt: string;
|
|
5
|
-
}
|
|
6
|
-
export declare function repoRegistryFile(): string;
|
|
7
|
-
export declare function parseRepoRegistry(raw: string): readonly RepoEntry[];
|
|
8
|
-
export declare function readRepoRegistry(): Promise<readonly RepoEntry[]>;
|
|
9
|
-
export declare function rememberRepo(entry: RepoEntry): Promise<readonly RepoEntry[]>;
|
|
10
|
-
export declare function noteCheckout(originUrl: string, path: string, now?: Date): Promise<void>;
|
|
11
|
-
export declare function forgetRepo(path: string): Promise<readonly RepoEntry[]>;
|
package/dist/repos.js
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { ensureConfigDir, updateFileLocked } from './lock.js';
|
|
4
|
-
import { normalizeOriginUrl } from './origin.js';
|
|
5
|
-
import { harborDir } from './paths.js';
|
|
6
|
-
export function repoRegistryFile() {
|
|
7
|
-
return join(harborDir(), 'repos.json');
|
|
8
|
-
}
|
|
9
|
-
function parseEntry(raw) {
|
|
10
|
-
if (raw === null || typeof raw !== 'object')
|
|
11
|
-
return [];
|
|
12
|
-
const obj = raw;
|
|
13
|
-
const originUrl = typeof obj.originUrl === 'string' ? normalizeOriginUrl(obj.originUrl) : '';
|
|
14
|
-
const path = typeof obj.path === 'string' ? obj.path : '';
|
|
15
|
-
if (originUrl === '' || path === '')
|
|
16
|
-
return [];
|
|
17
|
-
return [
|
|
18
|
-
{
|
|
19
|
-
originUrl,
|
|
20
|
-
path,
|
|
21
|
-
addedAt: typeof obj.addedAt === 'string' ? obj.addedAt : '',
|
|
22
|
-
},
|
|
23
|
-
];
|
|
24
|
-
}
|
|
25
|
-
export function parseRepoRegistry(raw) {
|
|
26
|
-
if (raw.trim() === '')
|
|
27
|
-
return [];
|
|
28
|
-
let parsed;
|
|
29
|
-
try {
|
|
30
|
-
parsed = JSON.parse(raw);
|
|
31
|
-
}
|
|
32
|
-
catch {
|
|
33
|
-
return [];
|
|
34
|
-
}
|
|
35
|
-
if (parsed === null || typeof parsed !== 'object')
|
|
36
|
-
return [];
|
|
37
|
-
const rows = parsed.repos;
|
|
38
|
-
if (!Array.isArray(rows))
|
|
39
|
-
return [];
|
|
40
|
-
return dedupe(rows.flatMap(parseEntry));
|
|
41
|
-
}
|
|
42
|
-
export async function readRepoRegistry() {
|
|
43
|
-
try {
|
|
44
|
-
return parseRepoRegistry(await readFile(repoRegistryFile(), 'utf8'));
|
|
45
|
-
}
|
|
46
|
-
catch {
|
|
47
|
-
return [];
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
function dedupe(entries) {
|
|
51
|
-
const byPath = new Map();
|
|
52
|
-
for (const entry of entries)
|
|
53
|
-
byPath.set(entry.path, entry);
|
|
54
|
-
return [...byPath.values()];
|
|
55
|
-
}
|
|
56
|
-
async function update(change) {
|
|
57
|
-
await ensureConfigDir(harborDir());
|
|
58
|
-
let next = [];
|
|
59
|
-
await updateFileLocked(repoRegistryFile(), (existing) => {
|
|
60
|
-
next = dedupe(change(parseRepoRegistry(existing)));
|
|
61
|
-
const body = { version: 1, repos: next };
|
|
62
|
-
return `${JSON.stringify(body, null, 2)}\n`;
|
|
63
|
-
});
|
|
64
|
-
return next;
|
|
65
|
-
}
|
|
66
|
-
export async function rememberRepo(entry) {
|
|
67
|
-
return update((entries) => [...entries, entry]);
|
|
68
|
-
}
|
|
69
|
-
export async function noteCheckout(originUrl, path, now = new Date()) {
|
|
70
|
-
try {
|
|
71
|
-
await rememberRepo({
|
|
72
|
-
originUrl: normalizeOriginUrl(originUrl),
|
|
73
|
-
path,
|
|
74
|
-
addedAt: now.toISOString(),
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
export async function forgetRepo(path) {
|
|
81
|
-
return update((entries) => entries.filter((entry) => entry.path !== path));
|
|
82
|
-
}
|
package/dist/skills.d.ts
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import type { AgentId, AgentPresence } from './providers/detect.js';
|
|
2
|
-
export declare const SKILLS_DIR = ".claude/skills";
|
|
3
|
-
export interface SkillPayload {
|
|
4
|
-
readonly id: string;
|
|
5
|
-
readonly name: string;
|
|
6
|
-
readonly path?: string;
|
|
7
|
-
readonly content: string;
|
|
8
|
-
}
|
|
9
|
-
export type SkillResult = {
|
|
10
|
-
readonly id: string;
|
|
11
|
-
readonly file?: string;
|
|
12
|
-
readonly dir?: string;
|
|
13
|
-
readonly status: 'synced';
|
|
14
|
-
} | {
|
|
15
|
-
readonly id: string;
|
|
16
|
-
readonly file?: string;
|
|
17
|
-
readonly dir?: string;
|
|
18
|
-
readonly status: 'skipped_theirs';
|
|
19
|
-
} | {
|
|
20
|
-
readonly id?: string;
|
|
21
|
-
readonly file?: string;
|
|
22
|
-
readonly dir?: string;
|
|
23
|
-
readonly status: 'forked';
|
|
24
|
-
} | {
|
|
25
|
-
readonly file?: string;
|
|
26
|
-
readonly dir?: string;
|
|
27
|
-
readonly status: 'removed';
|
|
28
|
-
};
|
|
29
|
-
export interface SkillRootSummary {
|
|
30
|
-
readonly agent: AgentId;
|
|
31
|
-
readonly dir: string;
|
|
32
|
-
readonly results: readonly SkillResult[];
|
|
33
|
-
readonly reclaimed: boolean;
|
|
34
|
-
}
|
|
35
|
-
export interface SkillSyncSummary {
|
|
36
|
-
readonly dir: string;
|
|
37
|
-
readonly results: readonly SkillResult[];
|
|
38
|
-
readonly reclaimed: boolean;
|
|
39
|
-
}
|
|
40
|
-
export interface EnvSkillSyncSummary extends SkillSyncSummary {
|
|
41
|
-
readonly roots: readonly SkillRootSummary[];
|
|
42
|
-
}
|
|
43
|
-
export interface SkillSyncOptions {
|
|
44
|
-
readonly workspaceId?: string;
|
|
45
|
-
}
|
|
46
|
-
export declare function syncSkillFilesToEnv(skills: readonly SkillPayload[], opts?: SkillSyncOptions & {
|
|
47
|
-
readonly home?: string;
|
|
48
|
-
readonly agents?: AgentPresence;
|
|
49
|
-
}): Promise<EnvSkillSyncSummary>;
|
|
50
|
-
export declare function syncSkillFilesToRepo(repoPath: string, skills: readonly SkillPayload[], opts?: SkillSyncOptions & {
|
|
51
|
-
readonly home?: string;
|
|
52
|
-
readonly agents?: AgentPresence;
|
|
53
|
-
}): Promise<EnvSkillSyncSummary>;
|
|
54
|
-
export type SkillState = 'ours' | 'forked' | 'missing' | 'unverifiable';
|
|
55
|
-
export interface SkillStatus {
|
|
56
|
-
readonly name: string;
|
|
57
|
-
readonly state: SkillState;
|
|
58
|
-
}
|
|
59
|
-
export interface ManagedSkills {
|
|
60
|
-
readonly dir: string;
|
|
61
|
-
readonly managed: boolean;
|
|
62
|
-
readonly skills: readonly SkillStatus[];
|
|
63
|
-
}
|
|
64
|
-
export declare function inspectRepoSkillRoots(repoRoot: string, opts?: {
|
|
65
|
-
readonly home?: string;
|
|
66
|
-
readonly agents?: AgentPresence;
|
|
67
|
-
}): Promise<ManagedSkillRoot[]>;
|
|
68
|
-
export declare function inspectRepoSkills(repoRoot: string): Promise<ManagedSkills>;
|
|
69
|
-
export interface ManagedSkillRoot extends ManagedSkills {
|
|
70
|
-
readonly agent: AgentId;
|
|
71
|
-
}
|
|
72
|
-
export declare function inspectEnvSkillRoots(opts?: {
|
|
73
|
-
readonly home?: string;
|
|
74
|
-
readonly agents?: AgentPresence;
|
|
75
|
-
}): Promise<ManagedSkillRoot[]>;
|
|
76
|
-
export declare function skillsInState(managed: ManagedSkills, state: SkillState): readonly string[];
|
|
77
|
-
export declare function removeRepoSkillFiles(repoPath: string, opts?: {
|
|
78
|
-
readonly home?: string;
|
|
79
|
-
readonly agents?: AgentPresence;
|
|
80
|
-
}): Promise<{
|
|
81
|
-
readonly dir: string;
|
|
82
|
-
readonly results: readonly SkillResult[];
|
|
83
|
-
readonly status: 'cleaned' | 'no_dir';
|
|
84
|
-
}>;
|
package/dist/skills.js
DELETED
|
@@ -1,383 +0,0 @@
|
|
|
1
|
-
import { existsSync, promises as fsp } from 'node:fs';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { MANIFEST_FILE, hashDocument, parseManifest, renderManifest, } from './documents/manifest.js';
|
|
5
|
-
import { writeFileAtomic } from './lock.js';
|
|
6
|
-
import { REPO_SKILL_DIR, SKILL_FILE, resolveRepoSkillRoots, resolveSkillRoots, } from './providers/surfaces.js';
|
|
7
|
-
import { slugify } from './slugify.js';
|
|
8
|
-
export const SKILLS_DIR = '.claude/skills';
|
|
9
|
-
async function ensureDir(path) {
|
|
10
|
-
await fsp.mkdir(path, { recursive: true, mode: 0o700 });
|
|
11
|
-
if (process.platform !== 'win32') {
|
|
12
|
-
try {
|
|
13
|
-
await fsp.chmod(path, 0o700);
|
|
14
|
-
}
|
|
15
|
-
catch {
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
const EMPTY_CLAIM = {
|
|
20
|
-
present: false,
|
|
21
|
-
files: new Set(),
|
|
22
|
-
hashes: new Map(),
|
|
23
|
-
workspaceId: null,
|
|
24
|
-
};
|
|
25
|
-
async function readClaim(markerPath) {
|
|
26
|
-
const raw = await fsp.readFile(markerPath, 'utf8').catch(() => null);
|
|
27
|
-
if (raw === null)
|
|
28
|
-
return EMPTY_CLAIM;
|
|
29
|
-
const parsed = parseManifest(raw);
|
|
30
|
-
return {
|
|
31
|
-
present: true,
|
|
32
|
-
files: new Set(parsed.files),
|
|
33
|
-
hashes: parsed.hashes,
|
|
34
|
-
workspaceId: parsed.workspaceId,
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
async function writeClaim(markerPath, entries, workspaceId) {
|
|
38
|
-
const body = renderManifest(entries, workspaceId ?? null);
|
|
39
|
-
const current = await fsp.readFile(markerPath, 'utf8').catch(() => null);
|
|
40
|
-
if (current === body)
|
|
41
|
-
return;
|
|
42
|
-
await writeFileAtomic(markerPath, body);
|
|
43
|
-
}
|
|
44
|
-
async function ownershipOf(filePath, file, claim) {
|
|
45
|
-
const recorded = claim.hashes.get(file);
|
|
46
|
-
if (recorded === undefined)
|
|
47
|
-
return 'unknown';
|
|
48
|
-
const current = await fsp.readFile(filePath, 'utf8').catch(() => null);
|
|
49
|
-
if (current === null)
|
|
50
|
-
return 'unknown';
|
|
51
|
-
return hashDocument(current) === recorded ? 'ours' : 'forked';
|
|
52
|
-
}
|
|
53
|
-
function slugOf(skill) {
|
|
54
|
-
const fromPath = skill.path
|
|
55
|
-
?.split('/')
|
|
56
|
-
.filter((part) => part.length > 0 && part !== '.' && part !== '..')
|
|
57
|
-
.filter((part) => part !== SKILL_FILE)
|
|
58
|
-
.pop()
|
|
59
|
-
?.replace(/\.md$/, '');
|
|
60
|
-
const candidate = (fromPath ?? '').replace(/[/\\]/g, '').trim();
|
|
61
|
-
const traversal = /^\.+$/.test(candidate);
|
|
62
|
-
const slug = candidate.length > 0 && !traversal ? candidate : slugify(skill.name);
|
|
63
|
-
if (slug.length === 0) {
|
|
64
|
-
throw new Error(`Skill name '${skill.name}' produced an empty slug — provide a name with at least one alphanumeric character`);
|
|
65
|
-
}
|
|
66
|
-
return slug;
|
|
67
|
-
}
|
|
68
|
-
export async function syncSkillFilesToEnv(skills, opts = {}) {
|
|
69
|
-
const home = opts.home ?? homedir();
|
|
70
|
-
const roots = await resolveSkillRoots(home, opts.agents);
|
|
71
|
-
const summaries = [];
|
|
72
|
-
for (const root of roots) {
|
|
73
|
-
summaries.push({
|
|
74
|
-
agent: root.agent,
|
|
75
|
-
...(await syncSkillsUnderRoot(root.dir, skills, opts)),
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
const primary = summaries.find((s) => s.agent === 'claude') ?? summaries[0] ?? null;
|
|
79
|
-
return {
|
|
80
|
-
dir: primary?.dir ?? join(home, SKILLS_DIR),
|
|
81
|
-
results: summaries.flatMap((s) => s.results),
|
|
82
|
-
reclaimed: summaries.some((s) => s.reclaimed),
|
|
83
|
-
roots: summaries,
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
async function syncSkillsUnderRoot(skillsDir, skills, opts) {
|
|
87
|
-
const markerPath = join(skillsDir, MANIFEST_FILE);
|
|
88
|
-
const previous = await readClaim(markerPath);
|
|
89
|
-
if (skills.length === 0 && !previous.present) {
|
|
90
|
-
return { dir: skillsDir, results: [], reclaimed: false };
|
|
91
|
-
}
|
|
92
|
-
if (skills.length === 0) {
|
|
93
|
-
const ours = opts.workspaceId !== undefined &&
|
|
94
|
-
previous.workspaceId !== null &&
|
|
95
|
-
previous.workspaceId === opts.workspaceId;
|
|
96
|
-
if (!ours)
|
|
97
|
-
return { dir: skillsDir, results: [], reclaimed: false };
|
|
98
|
-
const results = [];
|
|
99
|
-
for (const file of previous.files) {
|
|
100
|
-
if ((await ownershipOf(join(skillsDir, file), file, previous)) === 'forked') {
|
|
101
|
-
results.push({ file, ...slugPart(file), status: 'forked' });
|
|
102
|
-
continue;
|
|
103
|
-
}
|
|
104
|
-
await removeManagedFile(skillsDir, file);
|
|
105
|
-
results.push({ file, ...slugPart(file), status: 'removed' });
|
|
106
|
-
}
|
|
107
|
-
await fsp.rm(markerPath, { force: true });
|
|
108
|
-
return { dir: skillsDir, results, reclaimed: true };
|
|
109
|
-
}
|
|
110
|
-
await ensureDir(skillsDir);
|
|
111
|
-
const entries = [];
|
|
112
|
-
const results = [];
|
|
113
|
-
for (const skill of skills) {
|
|
114
|
-
const slug = slugOf(skill);
|
|
115
|
-
const file = `${slug}/${SKILL_FILE}`;
|
|
116
|
-
const skillDir = join(skillsDir, slug);
|
|
117
|
-
const filePath = join(skillDir, SKILL_FILE);
|
|
118
|
-
if (await isLink(skillDir)) {
|
|
119
|
-
results.push({ id: skill.id, dir: slug, file, status: 'skipped_theirs' });
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
const current = await fsp.readFile(filePath, 'utf8').catch(() => null);
|
|
123
|
-
if (current === skill.content) {
|
|
124
|
-
entries.push({ file, sha256: hashDocument(skill.content) });
|
|
125
|
-
results.push({ id: skill.id, dir: slug, file, status: 'synced' });
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
if (current !== null && !previous.files.has(file)) {
|
|
129
|
-
results.push({ id: skill.id, dir: slug, file, status: 'skipped_theirs' });
|
|
130
|
-
continue;
|
|
131
|
-
}
|
|
132
|
-
if (current !== null && (await ownershipOf(filePath, file, previous)) === 'forked') {
|
|
133
|
-
entries.push({ file, sha256: previous.hashes.get(file) ?? null });
|
|
134
|
-
results.push({ id: skill.id, dir: slug, file, status: 'forked' });
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
await ensureDir(skillDir);
|
|
138
|
-
await writeFileAtomic(filePath, skill.content);
|
|
139
|
-
entries.push({ file, sha256: hashDocument(skill.content) });
|
|
140
|
-
results.push({ id: skill.id, dir: slug, file, status: 'synced' });
|
|
141
|
-
}
|
|
142
|
-
if (!previous.present) {
|
|
143
|
-
for (const result of results) {
|
|
144
|
-
if (result.status !== 'synced' || result.dir === undefined)
|
|
145
|
-
continue;
|
|
146
|
-
const flat = `${result.dir}.md`;
|
|
147
|
-
const flatPath = join(skillsDir, flat);
|
|
148
|
-
if (!(await fsp.stat(flatPath).catch(() => null)))
|
|
149
|
-
continue;
|
|
150
|
-
await fsp.rm(flatPath, { force: true });
|
|
151
|
-
results.push({ dir: result.dir, file: flat, status: 'removed' });
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
const keep = new Set(entries.map((entry) => entry.file));
|
|
155
|
-
for (const stale of previous.files) {
|
|
156
|
-
if (keep.has(stale))
|
|
157
|
-
continue;
|
|
158
|
-
if ((await ownershipOf(join(skillsDir, stale), stale, previous)) === 'forked') {
|
|
159
|
-
results.push({ ...slugPart(stale), file: stale, status: 'forked' });
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
await removeManagedFile(skillsDir, stale);
|
|
163
|
-
results.push({ ...slugPart(stale), file: stale, status: 'removed' });
|
|
164
|
-
}
|
|
165
|
-
await writeClaim(markerPath, entries, opts.workspaceId);
|
|
166
|
-
return { dir: skillsDir, results, reclaimed: false };
|
|
167
|
-
}
|
|
168
|
-
function slugPart(file) {
|
|
169
|
-
const slash = file.indexOf('/');
|
|
170
|
-
return slash === -1 ? {} : { dir: file.slice(0, slash) };
|
|
171
|
-
}
|
|
172
|
-
async function removeManagedFile(root, file) {
|
|
173
|
-
await fsp.rm(join(root, file), { force: true });
|
|
174
|
-
const parent = slugPart(file).dir;
|
|
175
|
-
if (parent === undefined)
|
|
176
|
-
return;
|
|
177
|
-
await fsp.rmdir(join(root, parent)).catch(() => undefined);
|
|
178
|
-
}
|
|
179
|
-
async function isLink(path) {
|
|
180
|
-
return await fsp
|
|
181
|
-
.lstat(path)
|
|
182
|
-
.then((stats) => stats.isSymbolicLink())
|
|
183
|
-
.catch(() => false);
|
|
184
|
-
}
|
|
185
|
-
export async function syncSkillFilesToRepo(repoPath, skills, opts = {}) {
|
|
186
|
-
if (!existsSync(repoPath)) {
|
|
187
|
-
throw new Error(`Repo path does not exist: ${repoPath}`);
|
|
188
|
-
}
|
|
189
|
-
const roots = await resolveRepoSkillRoots(repoPath, {
|
|
190
|
-
...(opts.home === undefined ? {} : { home: opts.home }),
|
|
191
|
-
...(opts.agents === undefined ? {} : { agents: opts.agents }),
|
|
192
|
-
});
|
|
193
|
-
const summaries = [];
|
|
194
|
-
for (const root of roots) {
|
|
195
|
-
summaries.push({
|
|
196
|
-
agent: root.agent,
|
|
197
|
-
...(await syncSkillsUnderRepoRoot(root.dir, skills, opts)),
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
const primary = summaries.find((s) => s.agent === 'claude') ?? summaries[0] ?? null;
|
|
201
|
-
return {
|
|
202
|
-
dir: primary?.dir ?? join(repoPath, REPO_SKILL_DIR.claude),
|
|
203
|
-
results: summaries.flatMap((s) => s.results),
|
|
204
|
-
reclaimed: summaries.some((s) => s.reclaimed),
|
|
205
|
-
roots: summaries,
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
async function syncSkillsUnderRepoRoot(skillsDir, skills, opts) {
|
|
209
|
-
const expected = new Set();
|
|
210
|
-
const results = [];
|
|
211
|
-
if (skills.length > 0)
|
|
212
|
-
await ensureDir(skillsDir);
|
|
213
|
-
for (const skill of skills) {
|
|
214
|
-
const slug = slugOf(skill);
|
|
215
|
-
expected.add(slug);
|
|
216
|
-
const skillDir = join(skillsDir, slug);
|
|
217
|
-
const markerPath = join(skillDir, MANIFEST_FILE);
|
|
218
|
-
const filePath = join(skillDir, SKILL_FILE);
|
|
219
|
-
const claim = await readClaim(markerPath);
|
|
220
|
-
const current = await fsp.readFile(filePath, 'utf8').catch(() => null);
|
|
221
|
-
if (current !== null && !claim.present) {
|
|
222
|
-
results.push({ id: skill.id, dir: slug, status: 'skipped_theirs' });
|
|
223
|
-
continue;
|
|
224
|
-
}
|
|
225
|
-
if (current === skill.content) {
|
|
226
|
-
await writeClaim(markerPath, [{ file: SKILL_FILE, sha256: hashDocument(skill.content) }], opts.workspaceId);
|
|
227
|
-
results.push({ id: skill.id, dir: slug, status: 'synced' });
|
|
228
|
-
continue;
|
|
229
|
-
}
|
|
230
|
-
if (current !== null &&
|
|
231
|
-
(await ownershipOf(filePath, SKILL_FILE, claim)) === 'forked') {
|
|
232
|
-
await writeClaim(markerPath, [{ file: SKILL_FILE, sha256: claim.hashes.get(SKILL_FILE) ?? null }], claim.workspaceId ?? opts.workspaceId);
|
|
233
|
-
results.push({ id: skill.id, dir: slug, status: 'forked' });
|
|
234
|
-
continue;
|
|
235
|
-
}
|
|
236
|
-
await ensureDir(skillDir);
|
|
237
|
-
await writeFileAtomic(filePath, skill.content);
|
|
238
|
-
await writeClaim(markerPath, [{ file: SKILL_FILE, sha256: hashDocument(skill.content) }], opts.workspaceId);
|
|
239
|
-
results.push({ id: skill.id, dir: slug, status: 'synced' });
|
|
240
|
-
}
|
|
241
|
-
const pruned = await pruneRepoSkillDirs(skillsDir, expected, {
|
|
242
|
-
workspaceId: opts.workspaceId,
|
|
243
|
-
allowUnstamped: skills.length > 0,
|
|
244
|
-
});
|
|
245
|
-
results.push(...pruned);
|
|
246
|
-
const reclaimed = skills.length === 0 && results.some((r) => r.status === 'removed');
|
|
247
|
-
return { dir: skillsDir, results, reclaimed };
|
|
248
|
-
}
|
|
249
|
-
async function pruneRepoSkillDirs(skillsDir, expected, opts) {
|
|
250
|
-
const results = [];
|
|
251
|
-
const entries = await fsp
|
|
252
|
-
.readdir(skillsDir, { withFileTypes: true })
|
|
253
|
-
.catch(() => []);
|
|
254
|
-
for (const entry of entries) {
|
|
255
|
-
if (!entry.isDirectory())
|
|
256
|
-
continue;
|
|
257
|
-
if (expected.has(entry.name))
|
|
258
|
-
continue;
|
|
259
|
-
const dirPath = join(skillsDir, entry.name);
|
|
260
|
-
const claim = await readClaim(join(dirPath, MANIFEST_FILE));
|
|
261
|
-
if (!claim.present)
|
|
262
|
-
continue;
|
|
263
|
-
const stamped = opts.workspaceId !== undefined &&
|
|
264
|
-
claim.workspaceId !== null &&
|
|
265
|
-
claim.workspaceId === opts.workspaceId;
|
|
266
|
-
const otherWorkspace = claim.workspaceId !== null && claim.workspaceId !== opts.workspaceId;
|
|
267
|
-
if (!stamped && (otherWorkspace || !opts.allowUnstamped))
|
|
268
|
-
continue;
|
|
269
|
-
if ((await ownershipOf(join(dirPath, SKILL_FILE), SKILL_FILE, claim)) === 'forked') {
|
|
270
|
-
results.push({ dir: entry.name, status: 'forked' });
|
|
271
|
-
continue;
|
|
272
|
-
}
|
|
273
|
-
await fsp.rm(dirPath, { recursive: true, force: true });
|
|
274
|
-
results.push({ dir: entry.name, status: 'removed' });
|
|
275
|
-
}
|
|
276
|
-
return results;
|
|
277
|
-
}
|
|
278
|
-
function classify(claim, file, content) {
|
|
279
|
-
if (content === null)
|
|
280
|
-
return 'missing';
|
|
281
|
-
const recorded = claim.hashes.get(file);
|
|
282
|
-
if (recorded === undefined)
|
|
283
|
-
return 'unverifiable';
|
|
284
|
-
return hashDocument(content) === recorded ? 'ours' : 'forked';
|
|
285
|
-
}
|
|
286
|
-
export async function inspectRepoSkillRoots(repoRoot, opts = {}) {
|
|
287
|
-
const roots = await resolveRepoSkillRoots(repoRoot, opts);
|
|
288
|
-
const out = [];
|
|
289
|
-
for (const root of roots) {
|
|
290
|
-
out.push({ agent: root.agent, ...(await inspectSkillsUnderRoot(root.dir)) });
|
|
291
|
-
}
|
|
292
|
-
return out;
|
|
293
|
-
}
|
|
294
|
-
export async function inspectRepoSkills(repoRoot) {
|
|
295
|
-
return inspectSkillsUnderRoot(join(repoRoot, SKILLS_DIR));
|
|
296
|
-
}
|
|
297
|
-
async function inspectSkillsUnderRoot(dir) {
|
|
298
|
-
const entries = await fsp.readdir(dir, { withFileTypes: true }).catch(() => null);
|
|
299
|
-
if (entries === null)
|
|
300
|
-
return { dir, managed: false, skills: [] };
|
|
301
|
-
const skills = [];
|
|
302
|
-
for (const entry of entries) {
|
|
303
|
-
if (!entry.isDirectory())
|
|
304
|
-
continue;
|
|
305
|
-
const skillDir = join(dir, entry.name);
|
|
306
|
-
const claim = await readClaim(join(skillDir, MANIFEST_FILE));
|
|
307
|
-
if (!claim.present)
|
|
308
|
-
continue;
|
|
309
|
-
const content = await fsp
|
|
310
|
-
.readFile(join(skillDir, SKILL_FILE), 'utf8')
|
|
311
|
-
.catch(() => null);
|
|
312
|
-
skills.push({ name: entry.name, state: classify(claim, SKILL_FILE, content) });
|
|
313
|
-
}
|
|
314
|
-
return { dir, managed: skills.length > 0, skills };
|
|
315
|
-
}
|
|
316
|
-
export async function inspectEnvSkillRoots(opts = {}) {
|
|
317
|
-
const home = opts.home ?? homedir();
|
|
318
|
-
const roots = await resolveSkillRoots(home, opts.agents);
|
|
319
|
-
const out = [];
|
|
320
|
-
for (const root of roots) {
|
|
321
|
-
const claim = await readClaim(join(root.dir, MANIFEST_FILE));
|
|
322
|
-
if (!claim.present) {
|
|
323
|
-
out.push({ agent: root.agent, dir: root.dir, managed: false, skills: [] });
|
|
324
|
-
continue;
|
|
325
|
-
}
|
|
326
|
-
const skills = [];
|
|
327
|
-
for (const file of claim.files) {
|
|
328
|
-
const content = await fsp
|
|
329
|
-
.readFile(join(root.dir, file), 'utf8')
|
|
330
|
-
.catch(() => null);
|
|
331
|
-
skills.push({ name: file, state: classify(claim, file, content) });
|
|
332
|
-
}
|
|
333
|
-
out.push({ agent: root.agent, dir: root.dir, managed: true, skills });
|
|
334
|
-
}
|
|
335
|
-
return out;
|
|
336
|
-
}
|
|
337
|
-
export function skillsInState(managed, state) {
|
|
338
|
-
return managed.skills.filter((s) => s.state === state).map((s) => s.name);
|
|
339
|
-
}
|
|
340
|
-
export async function removeRepoSkillFiles(repoPath, opts = {}) {
|
|
341
|
-
if (!existsSync(repoPath)) {
|
|
342
|
-
throw new Error(`Repo path does not exist: ${repoPath}`);
|
|
343
|
-
}
|
|
344
|
-
const roots = await resolveRepoSkillRoots(repoPath, opts);
|
|
345
|
-
const results = [];
|
|
346
|
-
const cleaned = [];
|
|
347
|
-
for (const root of roots) {
|
|
348
|
-
if (!existsSync(root.dir))
|
|
349
|
-
continue;
|
|
350
|
-
results.push(...(await removeSkillsUnderRepoRoot(root.dir)));
|
|
351
|
-
cleaned.push(root.dir);
|
|
352
|
-
}
|
|
353
|
-
const primary = join(repoPath, REPO_SKILL_DIR.claude);
|
|
354
|
-
if (cleaned.length === 0) {
|
|
355
|
-
return { dir: primary, results: [], status: 'no_dir' };
|
|
356
|
-
}
|
|
357
|
-
return { dir: cleaned.includes(primary) ? primary : cleaned[0], results, status: 'cleaned' };
|
|
358
|
-
}
|
|
359
|
-
async function removeSkillsUnderRepoRoot(skillsDir) {
|
|
360
|
-
const results = [];
|
|
361
|
-
const entries = await fsp
|
|
362
|
-
.readdir(skillsDir, { withFileTypes: true })
|
|
363
|
-
.catch(() => []);
|
|
364
|
-
for (const entry of entries) {
|
|
365
|
-
if (!entry.isDirectory())
|
|
366
|
-
continue;
|
|
367
|
-
const dirPath = join(skillsDir, entry.name);
|
|
368
|
-
const claim = await readClaim(join(dirPath, MANIFEST_FILE));
|
|
369
|
-
if (!claim.present)
|
|
370
|
-
continue;
|
|
371
|
-
if ((await ownershipOf(join(dirPath, SKILL_FILE), SKILL_FILE, claim)) === 'forked') {
|
|
372
|
-
results.push({ dir: entry.name, status: 'forked' });
|
|
373
|
-
continue;
|
|
374
|
-
}
|
|
375
|
-
await fsp.rm(dirPath, { recursive: true, force: true });
|
|
376
|
-
results.push({ dir: entry.name, status: 'removed' });
|
|
377
|
-
}
|
|
378
|
-
const remaining = await fsp.readdir(skillsDir).catch(() => null);
|
|
379
|
-
if (remaining && remaining.length === 0) {
|
|
380
|
-
await fsp.rmdir(skillsDir).catch(() => undefined);
|
|
381
|
-
}
|
|
382
|
-
return results;
|
|
383
|
-
}
|
package/dist/slugify.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function slugify(name: string): string;
|