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/memories.js
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { promises as fsp } from 'node:fs';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { writeFileAtomic } from './lock.js';
|
|
5
|
-
import { slugify } from './slugify.js';
|
|
6
|
-
export function encodeProjectPath(projectPath) {
|
|
7
|
-
return projectPath.replaceAll('/', '-');
|
|
8
|
-
}
|
|
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
|
-
function isTeamMemoryFile(name) {
|
|
20
|
-
return name.startsWith('team--') && name.endsWith('.md');
|
|
21
|
-
}
|
|
22
|
-
export async function syncMemoryFilesToHome(opts) {
|
|
23
|
-
if (opts.projectPath.length === 0) {
|
|
24
|
-
throw new Error('projectPath is required');
|
|
25
|
-
}
|
|
26
|
-
const home = opts.home ?? homedir();
|
|
27
|
-
const encoded = encodeProjectPath(opts.projectPath);
|
|
28
|
-
const memoryDir = join(home, '.claude', 'projects', encoded, 'memory');
|
|
29
|
-
await ensureDir(memoryDir);
|
|
30
|
-
const expected = new Set();
|
|
31
|
-
const results = [];
|
|
32
|
-
for (const mem of opts.memories) {
|
|
33
|
-
const slug = slugify(mem.name);
|
|
34
|
-
if (slug.length === 0) {
|
|
35
|
-
throw new Error(`Memory name '${mem.name}' produced an empty slug — provide a name with at least one alphanumeric character`);
|
|
36
|
-
}
|
|
37
|
-
const filename = `team--${slug}.md`;
|
|
38
|
-
expected.add(filename);
|
|
39
|
-
const target = join(memoryDir, filename);
|
|
40
|
-
const current = await fsp.readFile(target, 'utf8').catch(() => null);
|
|
41
|
-
if (current !== mem.content) {
|
|
42
|
-
await writeFileAtomic(target, mem.content);
|
|
43
|
-
}
|
|
44
|
-
results.push({ id: mem.id, file: filename, status: 'synced' });
|
|
45
|
-
}
|
|
46
|
-
const entries = await fsp.readdir(memoryDir).catch(() => []);
|
|
47
|
-
for (const name of entries) {
|
|
48
|
-
if (!isTeamMemoryFile(name))
|
|
49
|
-
continue;
|
|
50
|
-
if (expected.has(name))
|
|
51
|
-
continue;
|
|
52
|
-
await fsp.unlink(join(memoryDir, name)).catch(() => undefined);
|
|
53
|
-
results.push({ file: name, status: 'removed' });
|
|
54
|
-
}
|
|
55
|
-
return { dir: memoryDir, results };
|
|
56
|
-
}
|
|
57
|
-
export async function removeProjectMemoryFiles(opts) {
|
|
58
|
-
if (opts.projectPath.length === 0) {
|
|
59
|
-
throw new Error('projectPath is required');
|
|
60
|
-
}
|
|
61
|
-
const home = opts.home ?? homedir();
|
|
62
|
-
const memoryDir = join(home, '.claude', 'projects', encodeProjectPath(opts.projectPath), 'memory');
|
|
63
|
-
const entries = await fsp.readdir(memoryDir).catch(() => null);
|
|
64
|
-
if (entries === null)
|
|
65
|
-
return { dir: memoryDir, results: [], status: 'no_dir' };
|
|
66
|
-
const results = [];
|
|
67
|
-
for (const name of entries) {
|
|
68
|
-
if (!isTeamMemoryFile(name))
|
|
69
|
-
continue;
|
|
70
|
-
await fsp.unlink(join(memoryDir, name)).catch(() => undefined);
|
|
71
|
-
results.push({ file: name, status: 'removed' });
|
|
72
|
-
}
|
|
73
|
-
return { dir: memoryDir, results, status: 'cleaned' };
|
|
74
|
-
}
|
package/dist/origin.d.ts
DELETED
package/dist/origin.js
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { execFileSync } from 'node:child_process';
|
|
2
|
-
export function normalizeOriginUrl(raw) {
|
|
3
|
-
let url = (raw ?? '').trim();
|
|
4
|
-
if (url === '')
|
|
5
|
-
return '';
|
|
6
|
-
const scpMatch = url.match(/^[^@/]+@([^:/]+):(.+)$/);
|
|
7
|
-
if (scpMatch) {
|
|
8
|
-
url = `https://${scpMatch[1]}/${scpMatch[2]}`;
|
|
9
|
-
}
|
|
10
|
-
else if (/^ssh:\/\//i.test(url)) {
|
|
11
|
-
url = url.replace(/^ssh:\/\//i, 'https://');
|
|
12
|
-
}
|
|
13
|
-
else if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(url)) {
|
|
14
|
-
url = `https://${url}`;
|
|
15
|
-
}
|
|
16
|
-
url = url.replace(/\.git$/i, '');
|
|
17
|
-
try {
|
|
18
|
-
const parsed = new URL(url);
|
|
19
|
-
parsed.username = '';
|
|
20
|
-
parsed.password = '';
|
|
21
|
-
parsed.hostname = parsed.hostname.toLowerCase().replace(/^www\./, '');
|
|
22
|
-
parsed.pathname = parsed.pathname.toLowerCase();
|
|
23
|
-
return parsed.toString().replace(/\/$/, '');
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
return url.toLowerCase().replace(/\/+$/, '');
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
export function gitOrigin(cwd) {
|
|
30
|
-
try {
|
|
31
|
-
const out = execFileSync('git', ['-C', cwd, 'remote', 'get-url', 'origin'], {
|
|
32
|
-
encoding: 'utf8',
|
|
33
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
34
|
-
timeout: 2000,
|
|
35
|
-
});
|
|
36
|
-
const trimmed = out.trim();
|
|
37
|
-
return trimmed === '' ? null : trimmed;
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
export function repoOrigin(cwd) {
|
|
44
|
-
return normalizeOriginUrl(gitOrigin(cwd));
|
|
45
|
-
}
|
|
46
|
-
export function gitRoot(cwd) {
|
|
47
|
-
try {
|
|
48
|
-
const out = execFileSync('git', ['-C', cwd, 'rev-parse', '--show-toplevel'], {
|
|
49
|
-
encoding: 'utf8',
|
|
50
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
51
|
-
timeout: 2000,
|
|
52
|
-
});
|
|
53
|
-
const trimmed = out.trim();
|
|
54
|
-
return trimmed === '' ? null : trimmed;
|
|
55
|
-
}
|
|
56
|
-
catch {
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
|
-
}
|
package/dist/paths.d.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
export declare function harborDir(): string;
|
|
2
|
-
export declare function legacyDirs(): readonly string[];
|
|
3
|
-
export declare function syncedOriginsFile(): string;
|
|
4
|
-
export declare function authFile(): string;
|
|
5
|
-
export declare function cliSessionsDir(): string;
|
|
6
|
-
export declare function captureDir(): string;
|
|
7
|
-
export declare function hooksBinDir(): string;
|
|
8
|
-
export declare function locksDir(): string;
|
|
9
|
-
export declare function recallOutcomesFile(): string;
|
|
10
|
-
export declare function machineFile(): string;
|
|
11
|
-
export declare function cliReleaseFile(): string;
|
|
12
|
-
export declare function upgradeNoticeStampFile(): string;
|
package/dist/paths.js
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { homedir } from 'node:os';
|
|
2
|
-
import { join, resolve } from 'node:path';
|
|
3
|
-
function harborHomeOverride() {
|
|
4
|
-
const raw = (process.env['HARBOR_HOME'] ?? process.env['ALEF0_HOME'])?.trim();
|
|
5
|
-
return raw !== undefined && raw.length > 0 ? resolve(raw) : null;
|
|
6
|
-
}
|
|
7
|
-
export function harborDir() {
|
|
8
|
-
return harborHomeOverride() ?? join(homedir(), '.harbor');
|
|
9
|
-
}
|
|
10
|
-
export function legacyDirs() {
|
|
11
|
-
const override = process.env['HARBOR_LEGACY_HOME']?.trim();
|
|
12
|
-
if (override && override.length > 0)
|
|
13
|
-
return [override];
|
|
14
|
-
return [join(homedir(), '.alef0'), join(homedir(), '.milestone')];
|
|
15
|
-
}
|
|
16
|
-
export function syncedOriginsFile() {
|
|
17
|
-
return join(harborDir(), 'synced-origins.json');
|
|
18
|
-
}
|
|
19
|
-
export function authFile() {
|
|
20
|
-
return join(harborDir(), 'auth.json');
|
|
21
|
-
}
|
|
22
|
-
export function cliSessionsDir() {
|
|
23
|
-
return join(harborDir(), 'cli-sessions');
|
|
24
|
-
}
|
|
25
|
-
export function captureDir() {
|
|
26
|
-
return join(harborDir(), 'capture');
|
|
27
|
-
}
|
|
28
|
-
export function hooksBinDir() {
|
|
29
|
-
return join(harborDir(), 'bin');
|
|
30
|
-
}
|
|
31
|
-
export function locksDir() {
|
|
32
|
-
return join(harborDir(), 'locks');
|
|
33
|
-
}
|
|
34
|
-
export function recallOutcomesFile() {
|
|
35
|
-
return join(harborDir(), 'recall-outcomes.jsonl');
|
|
36
|
-
}
|
|
37
|
-
export function machineFile() {
|
|
38
|
-
return join(harborDir(), 'machine.json');
|
|
39
|
-
}
|
|
40
|
-
export function cliReleaseFile() {
|
|
41
|
-
return join(harborDir(), 'cli-release.json');
|
|
42
|
-
}
|
|
43
|
-
export function upgradeNoticeStampFile() {
|
|
44
|
-
return join(harborDir(), 'upgrade-notified-at');
|
|
45
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
export declare const MCP_KEY = "harbor";
|
|
2
|
-
export declare const LEGACY_MCP_KEYS: readonly string[];
|
|
3
|
-
export declare const HOOK_CONTEXT_BASENAME = "session-context.mjs";
|
|
4
|
-
export declare const HOOK_REPORT_BASENAME = "session-report.mjs";
|
|
5
|
-
export declare const HOOK_CAPTURE_BASENAME = "session-capture.mjs";
|
|
6
|
-
export declare const HOOK_TOOLCONTEXT_BASENAME = "session-toolcontext.mjs";
|
|
7
|
-
export declare const HOOK_COMPACT_BASENAME = "session-compact.mjs";
|
|
8
|
-
export declare const MANAGED_HOOK_EVENTS: readonly ["UserPromptSubmit", "SessionEnd", "Stop", "PostToolUse", "PreToolUse", "SessionStart"];
|
|
9
|
-
export interface ClaudePaths {
|
|
10
|
-
readonly claudeDir: string;
|
|
11
|
-
readonly settingsPath: string;
|
|
12
|
-
readonly mcpConfigPath: string;
|
|
13
|
-
}
|
|
14
|
-
export declare function resolveClaudePaths(home?: string): ClaudePaths;
|
|
15
|
-
export interface McpServerEntry {
|
|
16
|
-
readonly type: 'http';
|
|
17
|
-
readonly url: string;
|
|
18
|
-
readonly headers: Readonly<Record<string, string>>;
|
|
19
|
-
}
|
|
20
|
-
export declare const ORIGIN_HEADER = "x-harbor-origin";
|
|
21
|
-
export declare function buildMcpServerEntry(mcpUrl: string, token: string, originUrl?: string): McpServerEntry;
|
|
22
|
-
export declare function hasOurMcpEntry(existing: string): boolean;
|
|
23
|
-
export declare function mergeMcpServer(existing: string, mcpUrl: string, token: string): string;
|
|
24
|
-
export declare function mergeProjectMcpServer(existing: string, opts: {
|
|
25
|
-
readonly repoPath: string;
|
|
26
|
-
readonly mcpUrl: string;
|
|
27
|
-
readonly token: string;
|
|
28
|
-
readonly originUrl: string;
|
|
29
|
-
}): string;
|
|
30
|
-
export declare function removeMcpServer(existing: string, label?: string): string;
|
|
31
|
-
export declare function readInstalledToken(existing: string): string | null;
|
|
32
|
-
export declare function readInstalledMcpUrl(existing: string): string | null;
|
|
33
|
-
export declare function isOurHookCommand(command: unknown): boolean;
|
|
34
|
-
export interface HookPaths {
|
|
35
|
-
readonly contextHookPath: string;
|
|
36
|
-
readonly reportHookPath: string;
|
|
37
|
-
readonly captureHookPath: string;
|
|
38
|
-
readonly toolContextHookPath: string;
|
|
39
|
-
readonly compactHookPath: string;
|
|
40
|
-
}
|
|
41
|
-
export declare function mergeHooks(existing: string, paths: HookPaths): string;
|
|
42
|
-
export declare function removeHooks(existing: string): string;
|
|
43
|
-
export interface InstalledHooks {
|
|
44
|
-
readonly installed: boolean;
|
|
45
|
-
readonly commands: readonly string[];
|
|
46
|
-
readonly events: readonly string[];
|
|
47
|
-
}
|
|
48
|
-
export declare function readInstalledHooks(existing: string): InstalledHooks;
|
|
49
|
-
export declare function readClaudeSettings(home?: string): Promise<string>;
|
|
50
|
-
export declare function readClaudeMcpConfig(home?: string): Promise<string>;
|
|
51
|
-
export declare function installMcpServer(opts: {
|
|
52
|
-
readonly mcpUrl: string;
|
|
53
|
-
readonly token: string;
|
|
54
|
-
readonly home?: string;
|
|
55
|
-
readonly repoPath?: string;
|
|
56
|
-
readonly originUrl?: string;
|
|
57
|
-
}): Promise<string>;
|
|
58
|
-
export declare function installProjectOrigin(opts: {
|
|
59
|
-
readonly repoPath: string;
|
|
60
|
-
readonly originUrl: string;
|
|
61
|
-
readonly home?: string;
|
|
62
|
-
}): Promise<string | null>;
|
|
63
|
-
export declare function installHooks(paths: HookPaths, home?: string): Promise<string>;
|
|
64
|
-
export declare function uninstallHooks(home?: string): Promise<string>;
|
|
65
|
-
export declare function uninstallMcpServer(home?: string): Promise<string>;
|
package/dist/providers/claude.js
DELETED
|
@@ -1,412 +0,0 @@
|
|
|
1
|
-
import { homedir } from 'node:os';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { ensureConfigDir, readIfExists, updateFileLocked } from '../lock.js';
|
|
4
|
-
import { isUnrunnableCommandEntry } from './hook-entry.js';
|
|
5
|
-
export const MCP_KEY = 'harbor';
|
|
6
|
-
export const LEGACY_MCP_KEYS = ['alef0', 'milestone'];
|
|
7
|
-
function deleteAllOurKeys(servers) {
|
|
8
|
-
delete servers[MCP_KEY];
|
|
9
|
-
for (const legacy of LEGACY_MCP_KEYS)
|
|
10
|
-
delete servers[legacy];
|
|
11
|
-
}
|
|
12
|
-
export const HOOK_CONTEXT_BASENAME = 'session-context.mjs';
|
|
13
|
-
export const HOOK_REPORT_BASENAME = 'session-report.mjs';
|
|
14
|
-
export const HOOK_CAPTURE_BASENAME = 'session-capture.mjs';
|
|
15
|
-
export const HOOK_TOOLCONTEXT_BASENAME = 'session-toolcontext.mjs';
|
|
16
|
-
export const HOOK_COMPACT_BASENAME = 'session-compact.mjs';
|
|
17
|
-
const HOOK_CONTEXT_TIMEOUT_S = 8;
|
|
18
|
-
const HOOK_REPORT_TIMEOUT_S = 5;
|
|
19
|
-
const HOOK_CAPTURE_TIMEOUT_S = 10;
|
|
20
|
-
const HOOK_TOOLCONTEXT_TIMEOUT_S = 5;
|
|
21
|
-
const HOOK_COMPACT_TIMEOUT_S = 2;
|
|
22
|
-
export const MANAGED_HOOK_EVENTS = [
|
|
23
|
-
'UserPromptSubmit',
|
|
24
|
-
'SessionEnd',
|
|
25
|
-
'Stop',
|
|
26
|
-
'PostToolUse',
|
|
27
|
-
'PreToolUse',
|
|
28
|
-
'SessionStart',
|
|
29
|
-
];
|
|
30
|
-
const HOOK_EVENTS = MANAGED_HOOK_EVENTS;
|
|
31
|
-
const TOOLCONTEXT_EDIT_MATCHER = 'Write|Edit';
|
|
32
|
-
const TOOLCONTEXT_BASH_MATCHER = 'Bash';
|
|
33
|
-
export function resolveClaudePaths(home = homedir()) {
|
|
34
|
-
const claudeDir = join(home, '.claude');
|
|
35
|
-
return {
|
|
36
|
-
claudeDir,
|
|
37
|
-
settingsPath: join(claudeDir, 'settings.json'),
|
|
38
|
-
mcpConfigPath: join(home, '.claude.json'),
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
function parseRoot(existing, allowEmpty, label = 'settings.json') {
|
|
42
|
-
if (existing.trim().length === 0) {
|
|
43
|
-
if (allowEmpty)
|
|
44
|
-
return {};
|
|
45
|
-
throw new Error('Config is empty');
|
|
46
|
-
}
|
|
47
|
-
let root;
|
|
48
|
-
try {
|
|
49
|
-
root = JSON.parse(existing);
|
|
50
|
-
}
|
|
51
|
-
catch (err) {
|
|
52
|
-
throw new Error(`Failed to parse ${label}: ${err.message}`);
|
|
53
|
-
}
|
|
54
|
-
if (root === null || typeof root !== 'object' || Array.isArray(root)) {
|
|
55
|
-
throw new Error(`${label} root is not an object`);
|
|
56
|
-
}
|
|
57
|
-
return root;
|
|
58
|
-
}
|
|
59
|
-
const serialise = (root) => `${JSON.stringify(root, null, 2)}\n`;
|
|
60
|
-
export const ORIGIN_HEADER = 'x-harbor-origin';
|
|
61
|
-
export function buildMcpServerEntry(mcpUrl, token, originUrl) {
|
|
62
|
-
const headers = { Authorization: `Bearer ${token}` };
|
|
63
|
-
if (originUrl !== undefined && originUrl !== '')
|
|
64
|
-
headers[ORIGIN_HEADER] = originUrl;
|
|
65
|
-
return { type: 'http', url: mcpUrl, headers };
|
|
66
|
-
}
|
|
67
|
-
function originOfEntry(entry) {
|
|
68
|
-
if (entry === null || typeof entry !== 'object')
|
|
69
|
-
return undefined;
|
|
70
|
-
const headers = entry.headers;
|
|
71
|
-
if (headers === null || typeof headers !== 'object')
|
|
72
|
-
return undefined;
|
|
73
|
-
const origin = headers[ORIGIN_HEADER];
|
|
74
|
-
return typeof origin === 'string' && origin !== '' ? origin : undefined;
|
|
75
|
-
}
|
|
76
|
-
function ourProjectEntries(root) {
|
|
77
|
-
const projects = root.projects;
|
|
78
|
-
if (projects === null || typeof projects !== 'object' || Array.isArray(projects)) {
|
|
79
|
-
return [];
|
|
80
|
-
}
|
|
81
|
-
const found = [];
|
|
82
|
-
for (const [path, project] of Object.entries(projects)) {
|
|
83
|
-
if (project === null || typeof project !== 'object' || Array.isArray(project))
|
|
84
|
-
continue;
|
|
85
|
-
const servers = project.mcpServers;
|
|
86
|
-
if (servers === null || typeof servers !== 'object' || Array.isArray(servers))
|
|
87
|
-
continue;
|
|
88
|
-
const bag = servers;
|
|
89
|
-
if (MCP_KEY in bag || LEGACY_MCP_KEYS.some((legacy) => legacy in bag)) {
|
|
90
|
-
found.push({ path, servers: bag });
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return found;
|
|
94
|
-
}
|
|
95
|
-
export function hasOurMcpEntry(existing) {
|
|
96
|
-
if (existing.trim() === '')
|
|
97
|
-
return false;
|
|
98
|
-
let root;
|
|
99
|
-
try {
|
|
100
|
-
root = JSON.parse(existing);
|
|
101
|
-
}
|
|
102
|
-
catch {
|
|
103
|
-
return false;
|
|
104
|
-
}
|
|
105
|
-
const servers = root?.mcpServers;
|
|
106
|
-
if (servers === null || typeof servers !== 'object' || Array.isArray(servers))
|
|
107
|
-
return false;
|
|
108
|
-
const keys = servers;
|
|
109
|
-
return MCP_KEY in keys || LEGACY_MCP_KEYS.some((legacy) => legacy in keys);
|
|
110
|
-
}
|
|
111
|
-
export function mergeMcpServer(existing, mcpUrl, token) {
|
|
112
|
-
const root = parseRoot(existing, true, '.claude.json');
|
|
113
|
-
let servers = root.mcpServers;
|
|
114
|
-
if (servers === undefined) {
|
|
115
|
-
servers = {};
|
|
116
|
-
root.mcpServers = servers;
|
|
117
|
-
}
|
|
118
|
-
if (servers === null || typeof servers !== 'object' || Array.isArray(servers)) {
|
|
119
|
-
throw new Error('mcpServers is not an object');
|
|
120
|
-
}
|
|
121
|
-
deleteAllOurKeys(servers);
|
|
122
|
-
servers[MCP_KEY] = buildMcpServerEntry(mcpUrl, token);
|
|
123
|
-
for (const { servers: bag } of ourProjectEntries(root)) {
|
|
124
|
-
const origin = originOfEntry(bag[MCP_KEY]);
|
|
125
|
-
deleteAllOurKeys(bag);
|
|
126
|
-
bag[MCP_KEY] = buildMcpServerEntry(mcpUrl, token, origin);
|
|
127
|
-
}
|
|
128
|
-
return serialise(root);
|
|
129
|
-
}
|
|
130
|
-
export function mergeProjectMcpServer(existing, opts) {
|
|
131
|
-
const root = parseRoot(existing, true, '.claude.json');
|
|
132
|
-
let projects = root.projects;
|
|
133
|
-
if (projects === undefined) {
|
|
134
|
-
projects = {};
|
|
135
|
-
root.projects = projects;
|
|
136
|
-
}
|
|
137
|
-
if (projects === null || typeof projects !== 'object' || Array.isArray(projects)) {
|
|
138
|
-
throw new Error('projects is not an object');
|
|
139
|
-
}
|
|
140
|
-
const bag = projects;
|
|
141
|
-
let project = bag[opts.repoPath];
|
|
142
|
-
if (project === undefined || project === null || typeof project !== 'object' || Array.isArray(project)) {
|
|
143
|
-
project = {};
|
|
144
|
-
bag[opts.repoPath] = project;
|
|
145
|
-
}
|
|
146
|
-
const entry = project;
|
|
147
|
-
let servers = entry.mcpServers;
|
|
148
|
-
if (servers === undefined || servers === null || typeof servers !== 'object' || Array.isArray(servers)) {
|
|
149
|
-
servers = {};
|
|
150
|
-
entry.mcpServers = servers;
|
|
151
|
-
}
|
|
152
|
-
const bagServers = servers;
|
|
153
|
-
deleteAllOurKeys(bagServers);
|
|
154
|
-
bagServers[MCP_KEY] = buildMcpServerEntry(opts.mcpUrl, opts.token, opts.originUrl);
|
|
155
|
-
return serialise(root);
|
|
156
|
-
}
|
|
157
|
-
export function removeMcpServer(existing, label = '.claude.json') {
|
|
158
|
-
if (existing.trim() === '')
|
|
159
|
-
return existing;
|
|
160
|
-
const root = parseRoot(existing, false, label);
|
|
161
|
-
const servers = root.mcpServers;
|
|
162
|
-
if (servers !== null && typeof servers === 'object' && !Array.isArray(servers)) {
|
|
163
|
-
deleteAllOurKeys(servers);
|
|
164
|
-
}
|
|
165
|
-
for (const { servers: bag } of ourProjectEntries(root))
|
|
166
|
-
deleteAllOurKeys(bag);
|
|
167
|
-
return serialise(root);
|
|
168
|
-
}
|
|
169
|
-
export function readInstalledToken(existing) {
|
|
170
|
-
if (existing.trim() === '')
|
|
171
|
-
return null;
|
|
172
|
-
let root;
|
|
173
|
-
try {
|
|
174
|
-
root = JSON.parse(existing);
|
|
175
|
-
}
|
|
176
|
-
catch {
|
|
177
|
-
return null;
|
|
178
|
-
}
|
|
179
|
-
const servers = root?.mcpServers;
|
|
180
|
-
if (servers === null || typeof servers !== 'object')
|
|
181
|
-
return null;
|
|
182
|
-
const entry = servers[MCP_KEY];
|
|
183
|
-
if (entry === null || typeof entry !== 'object')
|
|
184
|
-
return null;
|
|
185
|
-
const headers = entry.headers;
|
|
186
|
-
if (headers === null || typeof headers !== 'object')
|
|
187
|
-
return null;
|
|
188
|
-
const auth = headers.Authorization;
|
|
189
|
-
if (typeof auth !== 'string' || auth.trim() === '')
|
|
190
|
-
return null;
|
|
191
|
-
return auth.startsWith('Bearer ') ? auth.slice('Bearer '.length) : auth;
|
|
192
|
-
}
|
|
193
|
-
export function readInstalledMcpUrl(existing) {
|
|
194
|
-
if (existing.trim() === '')
|
|
195
|
-
return null;
|
|
196
|
-
try {
|
|
197
|
-
const root = JSON.parse(existing);
|
|
198
|
-
const servers = root.mcpServers;
|
|
199
|
-
const entry = servers?.[MCP_KEY];
|
|
200
|
-
return typeof entry?.url === 'string' ? entry.url : null;
|
|
201
|
-
}
|
|
202
|
-
catch {
|
|
203
|
-
return null;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
export function isOurHookCommand(command) {
|
|
207
|
-
if (typeof command !== 'string')
|
|
208
|
-
return false;
|
|
209
|
-
const base = command.trim().split(/[\\/]/).pop();
|
|
210
|
-
return (base === HOOK_CONTEXT_BASENAME ||
|
|
211
|
-
base === HOOK_REPORT_BASENAME ||
|
|
212
|
-
base === HOOK_CAPTURE_BASENAME ||
|
|
213
|
-
base === HOOK_COMPACT_BASENAME ||
|
|
214
|
-
base === HOOK_TOOLCONTEXT_BASENAME);
|
|
215
|
-
}
|
|
216
|
-
function stripOurHooks(event) {
|
|
217
|
-
if (!Array.isArray(event))
|
|
218
|
-
return [];
|
|
219
|
-
const out = [];
|
|
220
|
-
for (const group of event) {
|
|
221
|
-
if (group === null || typeof group !== 'object' || Array.isArray(group)) {
|
|
222
|
-
out.push(group);
|
|
223
|
-
continue;
|
|
224
|
-
}
|
|
225
|
-
const g = group;
|
|
226
|
-
if (!Array.isArray(g.hooks)) {
|
|
227
|
-
out.push(group);
|
|
228
|
-
continue;
|
|
229
|
-
}
|
|
230
|
-
const kept = g.hooks.filter((h) => {
|
|
231
|
-
if (h === null || typeof h !== 'object' || Array.isArray(h))
|
|
232
|
-
return true;
|
|
233
|
-
if (isUnrunnableCommandEntry(h))
|
|
234
|
-
return false;
|
|
235
|
-
return !isOurHookCommand(h.command);
|
|
236
|
-
});
|
|
237
|
-
if (kept.length === 0)
|
|
238
|
-
continue;
|
|
239
|
-
out.push({ ...g, hooks: kept });
|
|
240
|
-
}
|
|
241
|
-
return out;
|
|
242
|
-
}
|
|
243
|
-
const commandHookGroup = (command, timeout) => ({
|
|
244
|
-
hooks: [{ type: 'command', command, timeout }],
|
|
245
|
-
});
|
|
246
|
-
const matchedHookGroup = (matcher, command, timeout) => ({
|
|
247
|
-
matcher,
|
|
248
|
-
hooks: [{ type: 'command', command, timeout }],
|
|
249
|
-
});
|
|
250
|
-
export function mergeHooks(existing, paths) {
|
|
251
|
-
const root = parseRoot(existing, true);
|
|
252
|
-
let hooks = root.hooks;
|
|
253
|
-
if (hooks === undefined) {
|
|
254
|
-
hooks = {};
|
|
255
|
-
root.hooks = hooks;
|
|
256
|
-
}
|
|
257
|
-
if (hooks === null || typeof hooks !== 'object' || Array.isArray(hooks)) {
|
|
258
|
-
throw new Error('hooks is not an object');
|
|
259
|
-
}
|
|
260
|
-
const h = hooks;
|
|
261
|
-
h.UserPromptSubmit = [
|
|
262
|
-
...stripOurHooks(h.UserPromptSubmit),
|
|
263
|
-
commandHookGroup(paths.contextHookPath, HOOK_CONTEXT_TIMEOUT_S),
|
|
264
|
-
];
|
|
265
|
-
h.SessionEnd = [
|
|
266
|
-
...stripOurHooks(h.SessionEnd),
|
|
267
|
-
commandHookGroup(paths.reportHookPath, HOOK_REPORT_TIMEOUT_S),
|
|
268
|
-
];
|
|
269
|
-
h.Stop = [
|
|
270
|
-
...stripOurHooks(h.Stop),
|
|
271
|
-
commandHookGroup(paths.captureHookPath, HOOK_CAPTURE_TIMEOUT_S),
|
|
272
|
-
];
|
|
273
|
-
h.PostToolUse = [
|
|
274
|
-
...stripOurHooks(h.PostToolUse),
|
|
275
|
-
matchedHookGroup(TOOLCONTEXT_EDIT_MATCHER, paths.toolContextHookPath, HOOK_TOOLCONTEXT_TIMEOUT_S),
|
|
276
|
-
];
|
|
277
|
-
h.PreToolUse = [
|
|
278
|
-
...stripOurHooks(h.PreToolUse),
|
|
279
|
-
matchedHookGroup(TOOLCONTEXT_BASH_MATCHER, paths.toolContextHookPath, HOOK_TOOLCONTEXT_TIMEOUT_S),
|
|
280
|
-
];
|
|
281
|
-
h.SessionStart = [
|
|
282
|
-
...stripOurHooks(h.SessionStart),
|
|
283
|
-
commandHookGroup(paths.compactHookPath, HOOK_COMPACT_TIMEOUT_S),
|
|
284
|
-
];
|
|
285
|
-
return serialise(root);
|
|
286
|
-
}
|
|
287
|
-
export function removeHooks(existing) {
|
|
288
|
-
if (existing.trim() === '')
|
|
289
|
-
return existing;
|
|
290
|
-
const root = parseRoot(existing, false);
|
|
291
|
-
const hooks = root.hooks;
|
|
292
|
-
if (hooks !== null && typeof hooks === 'object' && !Array.isArray(hooks)) {
|
|
293
|
-
const h = hooks;
|
|
294
|
-
for (const event of HOOK_EVENTS) {
|
|
295
|
-
if (!(event in h))
|
|
296
|
-
continue;
|
|
297
|
-
const stripped = stripOurHooks(h[event]);
|
|
298
|
-
if (stripped.length === 0)
|
|
299
|
-
delete h[event];
|
|
300
|
-
else
|
|
301
|
-
h[event] = stripped;
|
|
302
|
-
}
|
|
303
|
-
if (Object.keys(h).length === 0)
|
|
304
|
-
delete root.hooks;
|
|
305
|
-
}
|
|
306
|
-
return serialise(root);
|
|
307
|
-
}
|
|
308
|
-
export function readInstalledHooks(existing) {
|
|
309
|
-
const commands = [];
|
|
310
|
-
const events = [];
|
|
311
|
-
const empty = { installed: false, commands, events };
|
|
312
|
-
if (existing.trim() === '')
|
|
313
|
-
return empty;
|
|
314
|
-
let root;
|
|
315
|
-
try {
|
|
316
|
-
root = JSON.parse(existing);
|
|
317
|
-
}
|
|
318
|
-
catch {
|
|
319
|
-
return empty;
|
|
320
|
-
}
|
|
321
|
-
const hooks = root?.hooks;
|
|
322
|
-
if (hooks === null || typeof hooks !== 'object' || Array.isArray(hooks)) {
|
|
323
|
-
return empty;
|
|
324
|
-
}
|
|
325
|
-
for (const event of HOOK_EVENTS) {
|
|
326
|
-
const groups = hooks[event];
|
|
327
|
-
if (!Array.isArray(groups))
|
|
328
|
-
continue;
|
|
329
|
-
const before = commands.length;
|
|
330
|
-
for (const group of groups) {
|
|
331
|
-
if (group === null || typeof group !== 'object')
|
|
332
|
-
continue;
|
|
333
|
-
const inner = group.hooks;
|
|
334
|
-
if (!Array.isArray(inner))
|
|
335
|
-
continue;
|
|
336
|
-
for (const h of inner) {
|
|
337
|
-
if (h === null || typeof h !== 'object')
|
|
338
|
-
continue;
|
|
339
|
-
const command = h.command;
|
|
340
|
-
if (isOurHookCommand(command))
|
|
341
|
-
commands.push(String(command));
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
if (commands.length > before)
|
|
345
|
-
events.push(event);
|
|
346
|
-
}
|
|
347
|
-
return { installed: commands.length > 0, commands, events };
|
|
348
|
-
}
|
|
349
|
-
const ensureClaudeDir = ensureConfigDir;
|
|
350
|
-
export async function readClaudeSettings(home) {
|
|
351
|
-
return readIfExists(resolveClaudePaths(home).settingsPath);
|
|
352
|
-
}
|
|
353
|
-
export async function readClaudeMcpConfig(home) {
|
|
354
|
-
return readIfExists(resolveClaudePaths(home).mcpConfigPath);
|
|
355
|
-
}
|
|
356
|
-
export async function installMcpServer(opts) {
|
|
357
|
-
const paths = resolveClaudePaths(opts.home);
|
|
358
|
-
await ensureClaudeDir(paths.claudeDir);
|
|
359
|
-
await updateFileLocked(paths.mcpConfigPath, (existing) => mergeMcpServer(existing, opts.mcpUrl, opts.token));
|
|
360
|
-
if (opts.repoPath && opts.originUrl) {
|
|
361
|
-
await updateFileLocked(paths.mcpConfigPath, (existing) => mergeProjectMcpServer(existing, {
|
|
362
|
-
repoPath: opts.repoPath,
|
|
363
|
-
mcpUrl: opts.mcpUrl,
|
|
364
|
-
token: opts.token,
|
|
365
|
-
originUrl: opts.originUrl,
|
|
366
|
-
}));
|
|
367
|
-
}
|
|
368
|
-
await stripLegacyMcpEntry(paths.settingsPath);
|
|
369
|
-
return paths.mcpConfigPath;
|
|
370
|
-
}
|
|
371
|
-
export async function installProjectOrigin(opts) {
|
|
372
|
-
if (opts.originUrl === '')
|
|
373
|
-
return null;
|
|
374
|
-
const paths = resolveClaudePaths(opts.home);
|
|
375
|
-
const existing = await readIfExists(paths.mcpConfigPath);
|
|
376
|
-
const token = readInstalledToken(existing);
|
|
377
|
-
const mcpUrl = readInstalledMcpUrl(existing);
|
|
378
|
-
if (!token || !mcpUrl)
|
|
379
|
-
return null;
|
|
380
|
-
await updateFileLocked(paths.mcpConfigPath, (current) => mergeProjectMcpServer(current, {
|
|
381
|
-
repoPath: opts.repoPath,
|
|
382
|
-
mcpUrl,
|
|
383
|
-
token,
|
|
384
|
-
originUrl: opts.originUrl,
|
|
385
|
-
}));
|
|
386
|
-
return paths.mcpConfigPath;
|
|
387
|
-
}
|
|
388
|
-
async function stripLegacyMcpEntry(settingsPath) {
|
|
389
|
-
const existing = await readIfExists(settingsPath);
|
|
390
|
-
if (!hasOurMcpEntry(existing))
|
|
391
|
-
return;
|
|
392
|
-
await updateFileLocked(settingsPath, (current) => removeMcpServer(current, 'settings.json'));
|
|
393
|
-
}
|
|
394
|
-
export async function installHooks(paths, home) {
|
|
395
|
-
const claude = resolveClaudePaths(home);
|
|
396
|
-
await ensureClaudeDir(claude.claudeDir);
|
|
397
|
-
await updateFileLocked(claude.settingsPath, (existing) => mergeHooks(existing, paths));
|
|
398
|
-
return claude.settingsPath;
|
|
399
|
-
}
|
|
400
|
-
export async function uninstallHooks(home) {
|
|
401
|
-
const claude = resolveClaudePaths(home);
|
|
402
|
-
await updateFileLocked(claude.settingsPath, (existing) => removeHooks(existing));
|
|
403
|
-
return claude.settingsPath;
|
|
404
|
-
}
|
|
405
|
-
export async function uninstallMcpServer(home) {
|
|
406
|
-
const claude = resolveClaudePaths(home);
|
|
407
|
-
if (hasOurMcpEntry(await readIfExists(claude.mcpConfigPath))) {
|
|
408
|
-
await updateFileLocked(claude.mcpConfigPath, (existing) => removeMcpServer(existing));
|
|
409
|
-
}
|
|
410
|
-
await stripLegacyMcpEntry(claude.settingsPath);
|
|
411
|
-
return claude.mcpConfigPath;
|
|
412
|
-
}
|