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
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
export interface ToggleOptions {
|
|
2
|
-
readonly cwd: string;
|
|
3
|
-
readonly captureOnly?: boolean;
|
|
4
|
-
readonly receiptOnly?: boolean;
|
|
5
|
-
readonly global?: boolean;
|
|
6
|
-
}
|
|
7
|
-
export interface ToggleResult {
|
|
8
|
-
readonly scope: 'repo' | 'global' | 'receipt';
|
|
9
|
-
readonly originUrl: string | null;
|
|
10
|
-
readonly recall: boolean;
|
|
11
|
-
readonly capture: boolean;
|
|
12
|
-
readonly receipt: boolean;
|
|
13
|
-
}
|
|
14
|
-
export declare function runOff(opts: ToggleOptions): Promise<ToggleResult>;
|
|
15
|
-
export declare function runOn(opts: ToggleOptions): Promise<ToggleResult>;
|
|
16
|
-
export declare function renderToggle(result: ToggleResult): string;
|
package/dist/commands/toggle.js
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { repoOrigin } from '../origin.js';
|
|
2
|
-
import { resolveArms, updateSyncedConfig, withGlobalPause, withReceipt, withRepoPause, } from '../state.js';
|
|
3
|
-
import { NotARepoError } from './bind.js';
|
|
4
|
-
async function applyRepo(cwd, mode) {
|
|
5
|
-
const origin = repoOrigin(cwd);
|
|
6
|
-
if (origin === '')
|
|
7
|
-
throw new NotARepoError(cwd);
|
|
8
|
-
const config = await updateSyncedConfig((current) => withRepoPause(current, origin, mode));
|
|
9
|
-
return {
|
|
10
|
-
scope: 'repo',
|
|
11
|
-
originUrl: origin,
|
|
12
|
-
...resolveArms(config.pause, origin),
|
|
13
|
-
receipt: config.receipt,
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
async function applyReceipt(cwd, receipt) {
|
|
17
|
-
const config = await updateSyncedConfig((current) => withReceipt(current, receipt));
|
|
18
|
-
const origin = repoOrigin(cwd);
|
|
19
|
-
return {
|
|
20
|
-
scope: 'receipt',
|
|
21
|
-
originUrl: origin === '' ? null : origin,
|
|
22
|
-
...resolveArms(config.pause, origin === '' ? ' no-repo' : origin),
|
|
23
|
-
receipt: config.receipt,
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
async function applyGlobal(mode) {
|
|
27
|
-
const config = await updateSyncedConfig((current) => withGlobalPause(current, mode));
|
|
28
|
-
const arms = resolveArms(config.pause, ' no-repo');
|
|
29
|
-
return { scope: 'global', originUrl: null, ...arms, receipt: config.receipt };
|
|
30
|
-
}
|
|
31
|
-
export function runOff(opts) {
|
|
32
|
-
if (opts.receiptOnly)
|
|
33
|
-
return applyReceipt(opts.cwd, false);
|
|
34
|
-
const mode = opts.captureOnly ? 'capture' : 'off';
|
|
35
|
-
return opts.global ? applyGlobal(mode) : applyRepo(opts.cwd, mode);
|
|
36
|
-
}
|
|
37
|
-
export async function runOn(opts) {
|
|
38
|
-
if (opts.receiptOnly)
|
|
39
|
-
return applyReceipt(opts.cwd, true);
|
|
40
|
-
if (opts.global)
|
|
41
|
-
return applyGlobal(null);
|
|
42
|
-
const origin = repoOrigin(opts.cwd);
|
|
43
|
-
if (origin === '')
|
|
44
|
-
throw new NotARepoError(opts.cwd);
|
|
45
|
-
const config = await updateSyncedConfig((current) => withRepoPause(current, origin, current.pause.global === null ? null : 'on'));
|
|
46
|
-
return {
|
|
47
|
-
scope: 'repo',
|
|
48
|
-
originUrl: origin,
|
|
49
|
-
...resolveArms(config.pause, origin),
|
|
50
|
-
receipt: config.receipt,
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
export function renderToggle(result) {
|
|
54
|
-
if (result.scope === 'receipt') {
|
|
55
|
-
const state = result.receipt
|
|
56
|
-
? 'This machine: the receipt is on. You will see which rules reached the agent.'
|
|
57
|
-
: 'This machine: the receipt is off. Rules still reach the agent, silently.';
|
|
58
|
-
return `${state}\n${renderArms(result)}`;
|
|
59
|
-
}
|
|
60
|
-
return renderArms(result);
|
|
61
|
-
}
|
|
62
|
-
function renderArms(result) {
|
|
63
|
-
const where = result.scope === 'repo' ? `This repo (${result.originUrl})` : 'This machine';
|
|
64
|
-
if (result.recall && result.capture)
|
|
65
|
-
return `${where}: recall on, recording on.`;
|
|
66
|
-
if (result.recall)
|
|
67
|
-
return `${where}: recall on, recording off.`;
|
|
68
|
-
return `${where}: paused. No context in, nothing recorded.`;
|
|
69
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { type ReclaimRepoSummary } from '../reclaim.js';
|
|
2
|
-
import type { AgentPresence } from '../providers/detect.js';
|
|
3
|
-
export interface UnbindOptions {
|
|
4
|
-
readonly cwd: string;
|
|
5
|
-
readonly home?: string;
|
|
6
|
-
readonly agents?: AgentPresence;
|
|
7
|
-
}
|
|
8
|
-
export interface UnbindResult {
|
|
9
|
-
readonly repoPath: string;
|
|
10
|
-
readonly originUrl: string | null;
|
|
11
|
-
readonly summary: ReclaimRepoSummary;
|
|
12
|
-
}
|
|
13
|
-
export declare function runUnbind(opts: UnbindOptions): Promise<UnbindResult>;
|
|
14
|
-
export declare function renderUnbind(result: UnbindResult): string;
|
package/dist/commands/unbind.js
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { gitRoot, repoOrigin } from '../origin.js';
|
|
2
|
-
import { reclaimRepoFiles } from '../reclaim.js';
|
|
3
|
-
import { DOCS_DIR } from '../documents/manifest.js';
|
|
4
|
-
import { REPO_SKILL_LANE } from '../providers/surfaces.js';
|
|
5
|
-
export async function runUnbind(opts) {
|
|
6
|
-
const repoPath = gitRoot(opts.cwd) ?? opts.cwd;
|
|
7
|
-
const origin = repoOrigin(opts.cwd);
|
|
8
|
-
const summary = await reclaimRepoFiles(repoPath, {
|
|
9
|
-
...(opts.home === undefined ? {} : { home: opts.home }),
|
|
10
|
-
...(opts.agents === undefined ? {} : { agents: opts.agents }),
|
|
11
|
-
});
|
|
12
|
-
return { repoPath, originUrl: origin === '' ? null : origin, summary };
|
|
13
|
-
}
|
|
14
|
-
export function renderUnbind(result) {
|
|
15
|
-
const { summary } = result;
|
|
16
|
-
const lines = [];
|
|
17
|
-
const count = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
18
|
-
const removed = (rows) => rows.filter((r) => r.status === 'removed').length;
|
|
19
|
-
const totalRemoved = removed(summary.skills) +
|
|
20
|
-
removed(summary.documents) +
|
|
21
|
-
removed(summary.pointer) +
|
|
22
|
-
removed(summary.memories);
|
|
23
|
-
if (totalRemoved === 0) {
|
|
24
|
-
lines.push(`Nothing left to take back from ${result.repoPath}.`);
|
|
25
|
-
}
|
|
26
|
-
else {
|
|
27
|
-
lines.push(`Unbound ${result.repoPath} from this machine.`);
|
|
28
|
-
const skills = removed(summary.skills);
|
|
29
|
-
if (skills > 0) {
|
|
30
|
-
lines.push(`${count(skills, 'skill', 'skills')} removed from ${REPO_SKILL_LANE}`);
|
|
31
|
-
}
|
|
32
|
-
const documents = removed(summary.documents);
|
|
33
|
-
if (documents > 0) {
|
|
34
|
-
lines.push(`${count(documents, 'document', 'documents')} removed from ${DOCS_DIR}`);
|
|
35
|
-
}
|
|
36
|
-
for (const pointer of summary.pointer) {
|
|
37
|
-
if (pointer.status !== 'removed')
|
|
38
|
-
continue;
|
|
39
|
-
lines.push(`Pointer line removed from ${pointer.file}`);
|
|
40
|
-
}
|
|
41
|
-
const memories = removed(summary.memories);
|
|
42
|
-
if (memories > 0) {
|
|
43
|
-
lines.push(`${count(memories, 'team memory', 'team memories')} removed from your Claude Code memory for this repo`);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
const forked = [
|
|
47
|
-
...summary.skills.filter((r) => r.status === 'forked').map((r) => r.dir ?? r.file),
|
|
48
|
-
...summary.documents.filter((r) => r.status === 'forked').map((r) => r.file),
|
|
49
|
-
].filter((name) => name !== undefined);
|
|
50
|
-
for (const name of forked) {
|
|
51
|
-
lines.push(` ${name} — you edited this one; left in place, not deleted`);
|
|
52
|
-
}
|
|
53
|
-
if (result.originUrl !== null) {
|
|
54
|
-
lines.push('');
|
|
55
|
-
lines.push(`Still bound in your workspace (${result.originUrl}) — \`harbor sync\`, or the desktop app if it syncs this repo, would write the team's files here again.`);
|
|
56
|
-
lines.push('Remove the binding itself in the webapp, under Repositories.');
|
|
57
|
-
}
|
|
58
|
-
lines.push('Recall and capture are unchanged — `harbor off` pauses this repo.');
|
|
59
|
-
return lines.join('\n');
|
|
60
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import type { Endpoints } from '../endpoints.js';
|
|
2
|
-
export interface WorkspacesOptions {
|
|
3
|
-
readonly endpoints: Endpoints;
|
|
4
|
-
readonly fetchImpl?: typeof fetch;
|
|
5
|
-
}
|
|
6
|
-
export interface WorkspaceRow {
|
|
7
|
-
readonly id: string;
|
|
8
|
-
readonly name: string;
|
|
9
|
-
readonly current: boolean;
|
|
10
|
-
}
|
|
11
|
-
export interface WorkspacesResult {
|
|
12
|
-
readonly workspaces: readonly WorkspaceRow[];
|
|
13
|
-
readonly currentId: string;
|
|
14
|
-
readonly currentIsStale: boolean;
|
|
15
|
-
readonly currentIsPending: boolean;
|
|
16
|
-
}
|
|
17
|
-
export declare function runWorkspaces(opts: WorkspacesOptions): Promise<WorkspacesResult>;
|
|
18
|
-
export declare function renderWorkspaces(result: WorkspacesResult): string;
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { listWorkspaces } from '../api.js';
|
|
2
|
-
import { isPendingWorkspace, readAuth } from '../auth/store.js';
|
|
3
|
-
import { NotSignedInError } from './bind.js';
|
|
4
|
-
export async function runWorkspaces(opts) {
|
|
5
|
-
const auth = await readAuth();
|
|
6
|
-
if (!auth)
|
|
7
|
-
throw new NotSignedInError();
|
|
8
|
-
const pending = isPendingWorkspace(auth);
|
|
9
|
-
const workspaces = await listWorkspaces(opts.endpoints.apiUrl, auth.runnerToken, opts.fetchImpl);
|
|
10
|
-
return {
|
|
11
|
-
workspaces: workspaces.map((w) => ({
|
|
12
|
-
id: w.id,
|
|
13
|
-
name: w.name,
|
|
14
|
-
current: w.id === auth.workspaceId,
|
|
15
|
-
})),
|
|
16
|
-
currentId: auth.workspaceId,
|
|
17
|
-
currentIsStale: !pending && !workspaces.some((w) => w.id === auth.workspaceId),
|
|
18
|
-
currentIsPending: pending,
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
export function renderWorkspaces(result) {
|
|
22
|
-
if (result.workspaces.length === 0) {
|
|
23
|
-
return 'This account is not a member of any workspace. Create one in the webapp first.';
|
|
24
|
-
}
|
|
25
|
-
const width = Math.max(...result.workspaces.map((w) => w.name.length));
|
|
26
|
-
const lines = result.workspaces.map((w) => {
|
|
27
|
-
const marker = w.current ? '*' : ' ';
|
|
28
|
-
return ` ${marker} ${w.name.padEnd(width)} ${w.id}`;
|
|
29
|
-
});
|
|
30
|
-
if (result.currentIsPending) {
|
|
31
|
-
lines.push('', ' ⚠ This machine is signed in but has no workspace yet. Choose one with', ' the command below — the browser is not needed again.');
|
|
32
|
-
}
|
|
33
|
-
else if (result.currentIsStale) {
|
|
34
|
-
lines.push('', ` ⚠ This machine is signed in to ${result.currentId}, which is not in the list`, ' above — you are no longer a member. Switch to one that is.');
|
|
35
|
-
}
|
|
36
|
-
const switchable = result.workspaces.filter((w) => !w.current);
|
|
37
|
-
if (switchable.length > 0 || result.currentIsStale || result.currentIsPending) {
|
|
38
|
-
lines.push('', `${result.currentIsPending ? 'Choose with' : 'Switch with'}: harbor login --workspace <id>`);
|
|
39
|
-
}
|
|
40
|
-
return lines.join('\n');
|
|
41
|
-
}
|
package/dist/context-budget.d.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { type AgentId, type AgentPresence } from './providers/detect.js';
|
|
2
|
-
export type Owner = 'harbor' | 'other-tool' | 'nobody';
|
|
3
|
-
export interface BudgetLine {
|
|
4
|
-
readonly id: 'skills' | 'skills-dark' | 'agents' | 'agent-files' | 'mcp' | 'injected';
|
|
5
|
-
readonly label: string;
|
|
6
|
-
readonly count: number | null;
|
|
7
|
-
readonly tokens: number | null;
|
|
8
|
-
readonly unmeasured?: string;
|
|
9
|
-
readonly note?: string;
|
|
10
|
-
}
|
|
11
|
-
export interface SkillEntry {
|
|
12
|
-
readonly name: string;
|
|
13
|
-
readonly path: string;
|
|
14
|
-
readonly owner: Owner;
|
|
15
|
-
readonly tokens: number;
|
|
16
|
-
readonly undiscoverable: boolean;
|
|
17
|
-
readonly copies: number;
|
|
18
|
-
}
|
|
19
|
-
export interface ContextBudget {
|
|
20
|
-
readonly lines: readonly BudgetLine[];
|
|
21
|
-
readonly measuredTokens: number;
|
|
22
|
-
readonly unmeasuredLines: number;
|
|
23
|
-
readonly skills: readonly SkillEntry[];
|
|
24
|
-
readonly ownership: Record<Owner, number>;
|
|
25
|
-
readonly undiscoverable: number;
|
|
26
|
-
}
|
|
27
|
-
export declare function estimateTokens(text: string): number;
|
|
28
|
-
export declare function indexCost(name: string, description: string): number;
|
|
29
|
-
export declare function readFrontmatter(content: string): {
|
|
30
|
-
name: string;
|
|
31
|
-
description: string;
|
|
32
|
-
};
|
|
33
|
-
export declare function collapseCopies(skills: readonly SkillEntry[]): SkillEntry[];
|
|
34
|
-
export interface ContextBudgetOptions {
|
|
35
|
-
readonly home?: string;
|
|
36
|
-
readonly repoPath?: string | null;
|
|
37
|
-
readonly agents?: AgentPresence;
|
|
38
|
-
}
|
|
39
|
-
export declare function measureContext(options?: ContextBudgetOptions): Promise<ContextBudget>;
|
|
40
|
-
export declare function presentAgents(present: AgentPresence): AgentId[];
|
package/dist/context-budget.js
DELETED
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
import { promises as fsp } from 'node:fs';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
|
-
import { basename, join } from 'node:path';
|
|
4
|
-
import { MANIFEST_FILE, parseManifest } from './documents/manifest.js';
|
|
5
|
-
import { resolveCodexPaths } from './providers/codex.js';
|
|
6
|
-
import { detectAgents } from './providers/detect.js';
|
|
7
|
-
import { AGENT_ROOT, REPO_AGENT_DIR, SKILL_FILE, resolveRepoSkillRoots, resolveSkillRoots, } from './providers/surfaces.js';
|
|
8
|
-
export function estimateTokens(text) {
|
|
9
|
-
return Math.ceil(text.length / 4);
|
|
10
|
-
}
|
|
11
|
-
const INDEX_ROW_OVERHEAD = 6;
|
|
12
|
-
export function indexCost(name, description) {
|
|
13
|
-
return estimateTokens(name) + estimateTokens(description) + INDEX_ROW_OVERHEAD;
|
|
14
|
-
}
|
|
15
|
-
export function readFrontmatter(content) {
|
|
16
|
-
const trimmed = content.trimStart();
|
|
17
|
-
if (!trimmed.startsWith('---'))
|
|
18
|
-
return { name: '', description: '' };
|
|
19
|
-
const end = trimmed.indexOf('\n---', 3);
|
|
20
|
-
const block = end === -1 ? trimmed : trimmed.slice(0, end);
|
|
21
|
-
const scalar = (key) => {
|
|
22
|
-
const match = block.match(new RegExp(`^${key}:\\s*(.*)$`, 'm'));
|
|
23
|
-
return match?.[1]?.trim().replace(/^["']|["']$/g, '') ?? '';
|
|
24
|
-
};
|
|
25
|
-
return { name: scalar('name'), description: scalar('description') };
|
|
26
|
-
}
|
|
27
|
-
async function skillsUnder(dir, owner) {
|
|
28
|
-
const entries = await fsp.readdir(dir, { withFileTypes: true }).catch(() => null);
|
|
29
|
-
if (entries === null)
|
|
30
|
-
return [];
|
|
31
|
-
const found = [];
|
|
32
|
-
for (const entry of entries) {
|
|
33
|
-
const path = entry.isDirectory()
|
|
34
|
-
? join(dir, entry.name, SKILL_FILE)
|
|
35
|
-
: join(dir, entry.name);
|
|
36
|
-
if (!entry.isDirectory() && !entry.name.endsWith('.md'))
|
|
37
|
-
continue;
|
|
38
|
-
const content = await fsp.readFile(path, 'utf8').catch(() => null);
|
|
39
|
-
if (content === null)
|
|
40
|
-
continue;
|
|
41
|
-
const { name, description } = readFrontmatter(content);
|
|
42
|
-
found.push({
|
|
43
|
-
name: entry.isDirectory() ? entry.name : basename(entry.name, '.md'),
|
|
44
|
-
path,
|
|
45
|
-
owner,
|
|
46
|
-
tokens: indexCost(name || entry.name, description),
|
|
47
|
-
undiscoverable: description.trim().length === 0,
|
|
48
|
-
copies: 1,
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
return found;
|
|
52
|
-
}
|
|
53
|
-
async function ourFiles(dir) {
|
|
54
|
-
const raw = await fsp.readFile(join(dir, MANIFEST_FILE), 'utf8').catch(() => null);
|
|
55
|
-
if (raw === null)
|
|
56
|
-
return new Set();
|
|
57
|
-
return new Set(parseManifest(raw).files);
|
|
58
|
-
}
|
|
59
|
-
const PLUGIN_DIR = 'plugins';
|
|
60
|
-
async function pluginSkillRoots(claudeDir) {
|
|
61
|
-
const root = join(claudeDir, PLUGIN_DIR);
|
|
62
|
-
const found = [];
|
|
63
|
-
const walk = async (dir, depth) => {
|
|
64
|
-
const entries = await fsp.readdir(dir, { withFileTypes: true }).catch(() => null);
|
|
65
|
-
if (entries === null)
|
|
66
|
-
return;
|
|
67
|
-
for (const entry of entries) {
|
|
68
|
-
if (!entry.isDirectory())
|
|
69
|
-
continue;
|
|
70
|
-
if (entry.name === 'skills') {
|
|
71
|
-
found.push(join(dir, entry.name));
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
if (depth > 0)
|
|
75
|
-
await walk(join(dir, entry.name), depth - 1);
|
|
76
|
-
}
|
|
77
|
-
};
|
|
78
|
-
await walk(root, 2);
|
|
79
|
-
return found;
|
|
80
|
-
}
|
|
81
|
-
async function agentDefinitions(dir) {
|
|
82
|
-
const entries = await fsp.readdir(dir, { withFileTypes: true }).catch(() => null);
|
|
83
|
-
if (entries === null)
|
|
84
|
-
return [];
|
|
85
|
-
const costs = [];
|
|
86
|
-
for (const entry of entries) {
|
|
87
|
-
if (entry.isDirectory() || !entry.name.endsWith('.md'))
|
|
88
|
-
continue;
|
|
89
|
-
const content = await fsp.readFile(join(dir, entry.name), 'utf8').catch(() => null);
|
|
90
|
-
if (content === null)
|
|
91
|
-
continue;
|
|
92
|
-
const { name, description } = readFrontmatter(content);
|
|
93
|
-
costs.push(indexCost(name || entry.name, description));
|
|
94
|
-
}
|
|
95
|
-
return costs;
|
|
96
|
-
}
|
|
97
|
-
async function wholeFileTokens(path) {
|
|
98
|
-
const content = await fsp.readFile(path, 'utf8').catch(() => null);
|
|
99
|
-
return content === null ? null : estimateTokens(content);
|
|
100
|
-
}
|
|
101
|
-
async function countMcpServers(claudeDir, repoPath) {
|
|
102
|
-
let total = 0;
|
|
103
|
-
const fromJson = async (path, key) => {
|
|
104
|
-
const raw = await fsp.readFile(path, 'utf8').catch(() => null);
|
|
105
|
-
if (raw === null)
|
|
106
|
-
return 0;
|
|
107
|
-
try {
|
|
108
|
-
const root = JSON.parse(raw);
|
|
109
|
-
if (root === null || typeof root !== 'object')
|
|
110
|
-
return 0;
|
|
111
|
-
const servers = root[key];
|
|
112
|
-
if (servers === null || typeof servers !== 'object')
|
|
113
|
-
return 0;
|
|
114
|
-
return Object.keys(servers).length;
|
|
115
|
-
}
|
|
116
|
-
catch {
|
|
117
|
-
return 0;
|
|
118
|
-
}
|
|
119
|
-
};
|
|
120
|
-
total += await fromJson(join(claudeDir, 'settings.json'), 'mcpServers');
|
|
121
|
-
total += await fromJson(join(claudeDir, 'mcp.json'), 'mcpServers');
|
|
122
|
-
if (repoPath)
|
|
123
|
-
total += await fromJson(join(repoPath, '.mcp.json'), 'mcpServers');
|
|
124
|
-
const codexConfig = await fsp
|
|
125
|
-
.readFile(resolveCodexPaths().configTomlPath, 'utf8')
|
|
126
|
-
.catch(() => null);
|
|
127
|
-
if (codexConfig !== null) {
|
|
128
|
-
total += (codexConfig.match(/^\[mcp_servers\.[^\]]+\]/gm) ?? []).length;
|
|
129
|
-
}
|
|
130
|
-
return total;
|
|
131
|
-
}
|
|
132
|
-
export function collapseCopies(skills) {
|
|
133
|
-
const byKey = new Map();
|
|
134
|
-
for (const skill of skills) {
|
|
135
|
-
const key = `${skill.name}\u0000${skill.owner}`;
|
|
136
|
-
const seen = byKey.get(key);
|
|
137
|
-
if (seen === undefined) {
|
|
138
|
-
byKey.set(key, skill);
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
byKey.set(key, {
|
|
142
|
-
...seen,
|
|
143
|
-
tokens: Math.max(seen.tokens, skill.tokens),
|
|
144
|
-
undiscoverable: seen.undiscoverable && skill.undiscoverable,
|
|
145
|
-
copies: seen.copies + skill.copies,
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
return Array.from(byKey.values());
|
|
149
|
-
}
|
|
150
|
-
export async function measureContext(options = {}) {
|
|
151
|
-
const home = options.home ?? homedir();
|
|
152
|
-
const repoPath = options.repoPath ?? null;
|
|
153
|
-
const present = options.agents ?? (await detectAgents(home));
|
|
154
|
-
const claudeDir = AGENT_ROOT.claude(home);
|
|
155
|
-
const found = [];
|
|
156
|
-
for (const root of await resolveSkillRoots(home, present)) {
|
|
157
|
-
const ours = await ourFiles(root.dir);
|
|
158
|
-
for (const skill of await skillsUnder(root.dir, 'nobody')) {
|
|
159
|
-
const rel = skill.path.slice(root.dir.length + 1);
|
|
160
|
-
found.push(ours.has(rel) ? { ...skill, owner: 'harbor' } : skill);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
if (repoPath) {
|
|
164
|
-
for (const root of await resolveRepoSkillRoots(repoPath, { home, agents: present })) {
|
|
165
|
-
for (const skill of await skillsUnder(root.dir, 'nobody')) {
|
|
166
|
-
const marked = await fsp
|
|
167
|
-
.access(join(root.dir, skill.name, MANIFEST_FILE))
|
|
168
|
-
.then(() => true)
|
|
169
|
-
.catch(() => false);
|
|
170
|
-
found.push(marked ? { ...skill, owner: 'harbor' } : skill);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
for (const pluginRoot of await pluginSkillRoots(claudeDir)) {
|
|
175
|
-
found.push(...(await skillsUnder(pluginRoot, 'other-tool')));
|
|
176
|
-
}
|
|
177
|
-
const agentCosts = [
|
|
178
|
-
...(await agentDefinitions(join(claudeDir, 'agents'))),
|
|
179
|
-
...(repoPath
|
|
180
|
-
? await agentDefinitions(join(repoPath, REPO_AGENT_DIR.claude, 'agents'))
|
|
181
|
-
: []),
|
|
182
|
-
];
|
|
183
|
-
const agentFilePaths = [
|
|
184
|
-
join(claudeDir, 'CLAUDE.md'),
|
|
185
|
-
join(resolveCodexPaths(home).codexDir, 'AGENTS.md'),
|
|
186
|
-
...(repoPath ? [join(repoPath, 'CLAUDE.md'), join(repoPath, 'AGENTS.md')] : []),
|
|
187
|
-
];
|
|
188
|
-
const agentFileTokens = [];
|
|
189
|
-
for (const path of agentFilePaths) {
|
|
190
|
-
const tokens = await wholeFileTokens(path);
|
|
191
|
-
if (tokens !== null)
|
|
192
|
-
agentFileTokens.push(tokens);
|
|
193
|
-
}
|
|
194
|
-
const mcpServers = await countMcpServers(claudeDir, repoPath);
|
|
195
|
-
const skills = collapseCopies(found);
|
|
196
|
-
const indexed = skills.filter((skill) => !skill.undiscoverable);
|
|
197
|
-
const undiscoverable = skills.length - indexed.length;
|
|
198
|
-
const ownership = {
|
|
199
|
-
harbor: indexed.filter((s) => s.owner === 'harbor').length,
|
|
200
|
-
'other-tool': indexed.filter((s) => s.owner === 'other-tool').length,
|
|
201
|
-
nobody: indexed.filter((s) => s.owner === 'nobody').length,
|
|
202
|
-
};
|
|
203
|
-
const lines = [
|
|
204
|
-
{
|
|
205
|
-
id: 'skills',
|
|
206
|
-
label: 'skills index',
|
|
207
|
-
count: indexed.length,
|
|
208
|
-
tokens: indexed.reduce((sum, skill) => sum + skill.tokens, 0),
|
|
209
|
-
note: `${ownership.harbor} managed by harbor · ${ownership.nobody} unowned · ${ownership['other-tool']} from other tools`,
|
|
210
|
-
},
|
|
211
|
-
{
|
|
212
|
-
id: 'skills-dark',
|
|
213
|
-
label: 'skill files not indexed',
|
|
214
|
-
count: undiscoverable,
|
|
215
|
-
tokens: null,
|
|
216
|
-
unmeasured: undiscoverable > 0
|
|
217
|
-
? 'no description — nothing can select them, and nothing here can price them'
|
|
218
|
-
: 'none',
|
|
219
|
-
},
|
|
220
|
-
{
|
|
221
|
-
id: 'agents',
|
|
222
|
-
label: 'agent definitions',
|
|
223
|
-
count: agentCosts.length,
|
|
224
|
-
tokens: agentCosts.reduce((sum, cost) => sum + cost, 0),
|
|
225
|
-
},
|
|
226
|
-
{
|
|
227
|
-
id: 'agent-files',
|
|
228
|
-
label: 'agent files',
|
|
229
|
-
count: agentFileTokens.length,
|
|
230
|
-
tokens: agentFileTokens.reduce((sum, cost) => sum + cost, 0),
|
|
231
|
-
},
|
|
232
|
-
{
|
|
233
|
-
id: 'mcp',
|
|
234
|
-
label: 'MCP tools',
|
|
235
|
-
count: mcpServers,
|
|
236
|
-
tokens: null,
|
|
237
|
-
unmeasured: 'tool descriptions live on the server — connect to price them',
|
|
238
|
-
note: `${mcpServers} server${mcpServers === 1 ? '' : 's'} configured`,
|
|
239
|
-
},
|
|
240
|
-
{
|
|
241
|
-
id: 'injected',
|
|
242
|
-
label: 'injected context',
|
|
243
|
-
count: null,
|
|
244
|
-
tokens: null,
|
|
245
|
-
unmeasured: 'varies per session — no on-disk record to read',
|
|
246
|
-
},
|
|
247
|
-
];
|
|
248
|
-
return {
|
|
249
|
-
lines,
|
|
250
|
-
measuredTokens: lines.reduce((sum, line) => sum + (line.tokens ?? 0), 0),
|
|
251
|
-
unmeasuredLines: lines.filter((line) => line.tokens === null).length,
|
|
252
|
-
skills,
|
|
253
|
-
ownership,
|
|
254
|
-
undiscoverable,
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
export function presentAgents(present) {
|
|
258
|
-
return Object.keys(present).filter((agent) => present[agent]);
|
|
259
|
-
}
|
package/dist/daemon/install.d.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
export declare const LAUNCHD_LABEL = "com.harbor.daemon";
|
|
2
|
-
export declare const SYSTEMD_UNIT = "harbor-daemon";
|
|
3
|
-
export type InstallPlatform = 'darwin' | 'linux' | 'unsupported';
|
|
4
|
-
export declare function installPlatform(platform?: string): InstallPlatform;
|
|
5
|
-
export interface DaemonProgram {
|
|
6
|
-
readonly interpreter: string;
|
|
7
|
-
readonly script: string;
|
|
8
|
-
}
|
|
9
|
-
export declare function daemonProgram(argv?: readonly string[], execPath?: string): DaemonProgram;
|
|
10
|
-
export declare function launchAgentPath(home?: string): string;
|
|
11
|
-
export declare function systemdDir(home?: string): string;
|
|
12
|
-
export declare function launchAgentPlist(program: DaemonProgram, harborHome: string, intervalSeconds?: number): string;
|
|
13
|
-
export declare function systemdService(program: DaemonProgram): string;
|
|
14
|
-
export declare function systemdTimer(intervalSeconds?: number): string;
|
|
15
|
-
export interface InstallPlan {
|
|
16
|
-
readonly platform: InstallPlatform;
|
|
17
|
-
readonly files: readonly {
|
|
18
|
-
readonly path: string;
|
|
19
|
-
readonly body: string;
|
|
20
|
-
}[];
|
|
21
|
-
readonly activate: readonly (readonly string[])[];
|
|
22
|
-
readonly deactivate: readonly (readonly string[])[];
|
|
23
|
-
}
|
|
24
|
-
export declare function installPlan(input: {
|
|
25
|
-
readonly program: DaemonProgram;
|
|
26
|
-
readonly harborHome: string;
|
|
27
|
-
readonly home?: string;
|
|
28
|
-
readonly platform?: InstallPlatform;
|
|
29
|
-
readonly uid?: number;
|
|
30
|
-
}): InstallPlan;
|
|
31
|
-
export interface InstallOutcome {
|
|
32
|
-
readonly kind: 'installed' | 'removed' | 'unsupported' | 'partial';
|
|
33
|
-
readonly files: readonly string[];
|
|
34
|
-
readonly manual: readonly (readonly string[])[];
|
|
35
|
-
readonly detail: string;
|
|
36
|
-
}
|
|
37
|
-
export type Runner = (argv: readonly string[]) => Promise<{
|
|
38
|
-
readonly ok: boolean;
|
|
39
|
-
readonly message: string;
|
|
40
|
-
}>;
|
|
41
|
-
export declare const spawnRunner: Runner;
|
|
42
|
-
export declare function installDaemon(input: {
|
|
43
|
-
readonly plan: InstallPlan;
|
|
44
|
-
readonly run?: Runner;
|
|
45
|
-
}): Promise<InstallOutcome>;
|
|
46
|
-
export declare function uninstallDaemon(input: {
|
|
47
|
-
readonly plan: InstallPlan;
|
|
48
|
-
readonly run?: Runner;
|
|
49
|
-
}): Promise<InstallOutcome>;
|
|
50
|
-
export declare function installedFiles(plan: InstallPlan): Promise<readonly string[]>;
|