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.
Files changed (120) hide show
  1. package/bin/checksums.json +12 -0
  2. package/bin/harbor.mjs +188 -0
  3. package/package.json +14 -30
  4. package/dist/api.d.ts +0 -109
  5. package/dist/api.js +0 -409
  6. package/dist/auth/login.d.ts +0 -14
  7. package/dist/auth/login.js +0 -158
  8. package/dist/auth/loopback.d.ts +0 -11
  9. package/dist/auth/loopback.js +0 -87
  10. package/dist/auth/pkce.d.ts +0 -7
  11. package/dist/auth/pkce.js +0 -14
  12. package/dist/auth/store.d.ts +0 -26
  13. package/dist/auth/store.js +0 -107
  14. package/dist/cli-release.d.ts +0 -31
  15. package/dist/cli-release.js +0 -102
  16. package/dist/commands/bind.d.ts +0 -23
  17. package/dist/commands/bind.js +0 -86
  18. package/dist/commands/context.d.ts +0 -12
  19. package/dist/commands/context.js +0 -80
  20. package/dist/commands/daemon.d.ts +0 -39
  21. package/dist/commands/daemon.js +0 -140
  22. package/dist/commands/doctor.d.ts +0 -16
  23. package/dist/commands/doctor.js +0 -937
  24. package/dist/commands/init.d.ts +0 -36
  25. package/dist/commands/init.js +0 -192
  26. package/dist/commands/status.d.ts +0 -5
  27. package/dist/commands/status.js +0 -164
  28. package/dist/commands/sync.d.ts +0 -40
  29. package/dist/commands/sync.js +0 -242
  30. package/dist/commands/toggle.d.ts +0 -16
  31. package/dist/commands/toggle.js +0 -69
  32. package/dist/commands/unbind.d.ts +0 -14
  33. package/dist/commands/unbind.js +0 -60
  34. package/dist/commands/workspaces.d.ts +0 -18
  35. package/dist/commands/workspaces.js +0 -41
  36. package/dist/context-budget.d.ts +0 -40
  37. package/dist/context-budget.js +0 -259
  38. package/dist/daemon/install.d.ts +0 -50
  39. package/dist/daemon/install.js +0 -192
  40. package/dist/daemon/loop.d.ts +0 -15
  41. package/dist/daemon/loop.js +0 -84
  42. package/dist/daemon/state.d.ts +0 -79
  43. package/dist/daemon/state.js +0 -213
  44. package/dist/daemon/tick.d.ts +0 -37
  45. package/dist/daemon/tick.js +0 -231
  46. package/dist/documents/apply.d.ts +0 -21
  47. package/dist/documents/apply.js +0 -58
  48. package/dist/documents/bundle.d.ts +0 -41
  49. package/dist/documents/bundle.js +0 -43
  50. package/dist/documents/inspect.d.ts +0 -12
  51. package/dist/documents/inspect.js +0 -33
  52. package/dist/documents/manifest.d.ts +0 -14
  53. package/dist/documents/manifest.js +0 -52
  54. package/dist/documents/pointer.d.ts +0 -32
  55. package/dist/documents/pointer.js +0 -135
  56. package/dist/documents/write.d.ts +0 -57
  57. package/dist/documents/write.js +0 -211
  58. package/dist/endpoints.d.ts +0 -7
  59. package/dist/endpoints.js +0 -15
  60. package/dist/env-bundle.d.ts +0 -25
  61. package/dist/env-bundle.js +0 -33
  62. package/dist/env-scan.d.ts +0 -27
  63. package/dist/env-scan.js +0 -41
  64. package/dist/env-sync.d.ts +0 -31
  65. package/dist/env-sync.js +0 -37
  66. package/dist/index.d.ts +0 -11
  67. package/dist/index.js +0 -282
  68. package/dist/install-hooks.d.ts +0 -11
  69. package/dist/install-hooks.js +0 -64
  70. package/dist/lock.d.ts +0 -31
  71. package/dist/lock.js +0 -192
  72. package/dist/machine.d.ts +0 -13
  73. package/dist/machine.js +0 -53
  74. package/dist/memories.d.ts +0 -31
  75. package/dist/memories.js +0 -74
  76. package/dist/origin.d.ts +0 -4
  77. package/dist/origin.js +0 -59
  78. package/dist/paths.d.ts +0 -12
  79. package/dist/paths.js +0 -45
  80. package/dist/providers/claude.d.ts +0 -65
  81. package/dist/providers/claude.js +0 -412
  82. package/dist/providers/codex-trust.d.ts +0 -36
  83. package/dist/providers/codex-trust.js +0 -141
  84. package/dist/providers/codex.d.ts +0 -46
  85. package/dist/providers/codex.js +0 -520
  86. package/dist/providers/cursor.d.ts +0 -45
  87. package/dist/providers/cursor.js +0 -344
  88. package/dist/providers/detect.d.ts +0 -16
  89. package/dist/providers/detect.js +0 -70
  90. package/dist/providers/hook-coverage.d.ts +0 -5
  91. package/dist/providers/hook-coverage.js +0 -47
  92. package/dist/providers/hook-entry.d.ts +0 -1
  93. package/dist/providers/hook-entry.js +0 -8
  94. package/dist/providers/surfaces.d.ts +0 -19
  95. package/dist/providers/surfaces.js +0 -91
  96. package/dist/reclaim.d.ts +0 -17
  97. package/dist/reclaim.js +0 -33
  98. package/dist/render.d.ts +0 -17
  99. package/dist/render.js +0 -61
  100. package/dist/repo-sync.d.ts +0 -27
  101. package/dist/repo-sync.js +0 -34
  102. package/dist/repos.d.ts +0 -11
  103. package/dist/repos.js +0 -82
  104. package/dist/skills.d.ts +0 -84
  105. package/dist/skills.js +0 -383
  106. package/dist/slugify.d.ts +0 -1
  107. package/dist/slugify.js +0 -8
  108. package/dist/state.d.ts +0 -23
  109. package/dist/state.js +0 -161
  110. package/dist/sync-report.d.ts +0 -59
  111. package/dist/sync-report.js +0 -87
  112. package/hooks/_recall.mjs +0 -288
  113. package/hooks/_receipt.mjs +0 -47
  114. package/hooks/_shared.mjs +0 -1610
  115. package/hooks/_upgrade.mjs +0 -51
  116. package/hooks/session-capture.mjs +0 -60
  117. package/hooks/session-compact.mjs +0 -19
  118. package/hooks/session-context.mjs +0 -258
  119. package/hooks/session-report.mjs +0 -42
  120. package/hooks/session-toolcontext.mjs +0 -183
package/dist/auth/pkce.js DELETED
@@ -1,14 +0,0 @@
1
- import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
2
- export function createPkcePair() {
3
- const verifier = randomBytes(32).toString('base64url');
4
- const challenge = createHash('sha256').update(verifier).digest('base64url');
5
- return { verifier, challenge };
6
- }
7
- export function createState() {
8
- return randomBytes(16).toString('base64url');
9
- }
10
- export function statesMatch(expected, received) {
11
- const a = Buffer.from(expected);
12
- const b = Buffer.from(received);
13
- return a.length === b.length && timingSafeEqual(a, b);
14
- }
@@ -1,26 +0,0 @@
1
- export interface StoredAuth {
2
- readonly runnerToken: string;
3
- readonly workspaceId: string;
4
- readonly workspaceName: string;
5
- readonly apiUrl: string;
6
- readonly signedInAt: string;
7
- readonly mcpMintedAt?: string;
8
- readonly mcpConnectionId?: string;
9
- }
10
- export declare function readAuth(): Promise<StoredAuth | null>;
11
- export declare function isPendingWorkspace(auth: StoredAuth): boolean;
12
- export interface Credentials {
13
- readonly runnerToken: string;
14
- readonly workspaceId: string;
15
- readonly source: 'env' | 'file';
16
- }
17
- export declare class PartialEnvCredentialError extends Error {
18
- constructor(missing: string);
19
- }
20
- export declare function resolveCredentials(env?: NodeJS.ProcessEnv): Promise<Credentials | null>;
21
- export declare function envShadowWarning(env?: NodeJS.ProcessEnv): string | null;
22
- export declare function writeAuth(auth: StoredAuth): Promise<void>;
23
- export declare function clearAuth(): Promise<void>;
24
- export declare function decodeJwtClaims(token: string): Record<string, unknown> | null;
25
- export declare function jwtExpiry(token: string): Date | null;
26
- export declare function isExpired(token: string, now?: Date): boolean;
@@ -1,107 +0,0 @@
1
- import { chmod, readFile } from 'node:fs/promises';
2
- import { updateFileLocked } from '../lock.js';
3
- import { authFile } from '../paths.js';
4
- function parse(raw) {
5
- let parsed;
6
- try {
7
- parsed = JSON.parse(raw);
8
- }
9
- catch {
10
- return null;
11
- }
12
- if (parsed === null || typeof parsed !== 'object')
13
- return null;
14
- const obj = parsed;
15
- if (typeof obj.runnerToken !== 'string' ||
16
- typeof obj.workspaceId !== 'string' ||
17
- typeof obj.apiUrl !== 'string') {
18
- return null;
19
- }
20
- return {
21
- runnerToken: obj.runnerToken,
22
- workspaceId: obj.workspaceId,
23
- workspaceName: typeof obj.workspaceName === 'string' ? obj.workspaceName : obj.workspaceId,
24
- apiUrl: obj.apiUrl,
25
- signedInAt: typeof obj.signedInAt === 'string' ? obj.signedInAt : '',
26
- mcpMintedAt: typeof obj.mcpMintedAt === 'string' ? obj.mcpMintedAt : undefined,
27
- mcpConnectionId: typeof obj.mcpConnectionId === 'string' ? obj.mcpConnectionId : undefined,
28
- };
29
- }
30
- export async function readAuth() {
31
- try {
32
- return parse(await readFile(authFile(), 'utf8'));
33
- }
34
- catch {
35
- return null;
36
- }
37
- }
38
- export function isPendingWorkspace(auth) {
39
- return auth.workspaceId === '';
40
- }
41
- export class PartialEnvCredentialError extends Error {
42
- constructor(missing) {
43
- super(`${missing} is not set. HARBOR_TOKEN and HARBOR_WORKSPACE_ID must be set together — ` +
44
- 'a token without a workspace would silently sync whichever workspace this machine last signed in to.');
45
- this.name = 'PartialEnvCredentialError';
46
- }
47
- }
48
- export async function resolveCredentials(env = process.env) {
49
- const token = (env['HARBOR_TOKEN'] ?? env['ALEF0_TOKEN'])?.trim();
50
- const workspaceId = (env['HARBOR_WORKSPACE_ID'] ?? env['ALEF0_WORKSPACE_ID'])?.trim();
51
- if (token || workspaceId) {
52
- if (!token)
53
- throw new PartialEnvCredentialError('HARBOR_TOKEN');
54
- if (!workspaceId)
55
- throw new PartialEnvCredentialError('HARBOR_WORKSPACE_ID');
56
- return { runnerToken: token, workspaceId, source: 'env' };
57
- }
58
- const stored = await readAuth();
59
- if (!stored || isPendingWorkspace(stored))
60
- return null;
61
- return {
62
- runnerToken: stored.runnerToken,
63
- workspaceId: stored.workspaceId,
64
- source: 'file',
65
- };
66
- }
67
- export function envShadowWarning(env = process.env) {
68
- const name = env['HARBOR_TOKEN']?.trim()
69
- ? 'HARBOR_TOKEN'
70
- : env['ALEF0_TOKEN']?.trim()
71
- ? 'ALEF0_TOKEN'
72
- : null;
73
- if (!name)
74
- return null;
75
- return (`⚠ ${name} is set in this environment, and it wins over the credential just saved. ` +
76
- `\`harbor sync\` and the daemon will keep using it until you unset it.`);
77
- }
78
- export async function writeAuth(auth) {
79
- await updateFileLocked(authFile(), () => `${JSON.stringify(auth, null, 2)}\n`);
80
- await chmod(authFile(), 0o600).catch(() => undefined);
81
- }
82
- export async function clearAuth() {
83
- await updateFileLocked(authFile(), () => '');
84
- }
85
- export function decodeJwtClaims(token) {
86
- const parts = token.split('.');
87
- if (parts.length !== 3)
88
- return null;
89
- try {
90
- const json = Buffer.from(parts[1], 'base64url').toString('utf8');
91
- const claims = JSON.parse(json);
92
- return claims !== null && typeof claims === 'object'
93
- ? claims
94
- : null;
95
- }
96
- catch {
97
- return null;
98
- }
99
- }
100
- export function jwtExpiry(token) {
101
- const exp = decodeJwtClaims(token)?.exp;
102
- return typeof exp === 'number' ? new Date(exp * 1000) : null;
103
- }
104
- export function isExpired(token, now = new Date()) {
105
- const expiry = jwtExpiry(token);
106
- return expiry !== null && expiry.getTime() <= now.getTime();
107
- }
@@ -1,31 +0,0 @@
1
- export declare const RELEASE_TTL_MS: number;
2
- export declare const UPGRADE_COMMAND = "npx harborloop@latest init";
3
- export interface CliRelease {
4
- readonly latest: string;
5
- readonly current: string;
6
- readonly behind: boolean;
7
- readonly command: string;
8
- readonly checkedAt: string;
9
- }
10
- export declare function compareVersions(a: string, b: string): number;
11
- export declare function fetchLatestVersion(fetchImpl?: typeof fetch): Promise<string | null>;
12
- export declare function readCliRelease(): Promise<CliRelease | null>;
13
- export type ReleaseCheck = {
14
- readonly kind: 'checked';
15
- readonly release: CliRelease;
16
- } | {
17
- readonly kind: 'cached';
18
- readonly release: CliRelease;
19
- } | {
20
- readonly kind: 'unreachable';
21
- } | {
22
- readonly kind: 'unknown-version';
23
- };
24
- export interface RefreshOptions {
25
- readonly now?: Date;
26
- readonly ttlMs?: number;
27
- readonly fetchImpl?: typeof fetch;
28
- readonly force?: boolean;
29
- }
30
- export declare function refreshCliRelease(opts?: RefreshOptions): Promise<ReleaseCheck>;
31
- export declare function describeRelease(check: ReleaseCheck): string | null;
@@ -1,102 +0,0 @@
1
- import { readFile } from 'node:fs/promises';
2
- import { apiRequest } from './api.js';
3
- import { ensureConfigDir, writeFileAtomic } from './lock.js';
4
- import { readCliVersion } from './machine.js';
5
- import { cliReleaseFile, harborDir } from './paths.js';
6
- const NPM_LATEST_URL = 'https://registry.npmjs.org/harborloop/latest';
7
- export const RELEASE_TTL_MS = 6 * 60 * 60 * 1000;
8
- const FETCH_TIMEOUT_MS = 5_000;
9
- export const UPGRADE_COMMAND = 'npx harborloop@latest init';
10
- export function compareVersions(a, b) {
11
- const [coreA = '', preA = ''] = a.trim().split('-', 2);
12
- const [coreB = '', preB = ''] = b.trim().split('-', 2);
13
- const partsA = coreA.split('.');
14
- const partsB = coreB.split('.');
15
- const width = Math.max(partsA.length, partsB.length);
16
- for (let i = 0; i < width; i += 1) {
17
- const numA = Number.parseInt(partsA[i] ?? '0', 10);
18
- const numB = Number.parseInt(partsB[i] ?? '0', 10);
19
- const left = Number.isFinite(numA) ? numA : 0;
20
- const right = Number.isFinite(numB) ? numB : 0;
21
- if (left !== right)
22
- return left - right;
23
- }
24
- if (preA === preB)
25
- return 0;
26
- if (preA === '')
27
- return 1;
28
- if (preB === '')
29
- return -1;
30
- return preA < preB ? -1 : 1;
31
- }
32
- export async function fetchLatestVersion(fetchImpl) {
33
- try {
34
- const body = await apiRequest(NPM_LATEST_URL, {
35
- timeoutMs: FETCH_TIMEOUT_MS,
36
- ...(fetchImpl ? { fetchImpl } : {}),
37
- });
38
- const version = body?.version;
39
- return typeof version === 'string' && version.trim() !== '' ? version.trim() : null;
40
- }
41
- catch {
42
- return null;
43
- }
44
- }
45
- export async function readCliRelease() {
46
- let raw;
47
- try {
48
- raw = await readFile(cliReleaseFile(), 'utf8');
49
- }
50
- catch {
51
- return null;
52
- }
53
- try {
54
- const parsed = JSON.parse(raw);
55
- if (typeof parsed.latest !== 'string' || typeof parsed.current !== 'string')
56
- return null;
57
- return {
58
- latest: parsed.latest,
59
- current: parsed.current,
60
- behind: parsed.behind === true,
61
- command: typeof parsed.command === 'string' ? parsed.command : UPGRADE_COMMAND,
62
- checkedAt: typeof parsed.checkedAt === 'string' ? parsed.checkedAt : '',
63
- };
64
- }
65
- catch {
66
- return null;
67
- }
68
- }
69
- export async function refreshCliRelease(opts = {}) {
70
- const now = opts.now ?? new Date();
71
- const ttlMs = opts.ttlMs ?? RELEASE_TTL_MS;
72
- const stored = await readCliRelease();
73
- if (!opts.force && stored && stored.checkedAt !== '') {
74
- const age = now.getTime() - Date.parse(stored.checkedAt);
75
- if (Number.isFinite(age) && age < ttlMs)
76
- return { kind: 'cached', release: stored };
77
- }
78
- const current = await readCliVersion();
79
- if (current === 'unknown')
80
- return { kind: 'unknown-version' };
81
- const latest = await fetchLatestVersion(opts.fetchImpl);
82
- if (latest === null)
83
- return { kind: 'unreachable' };
84
- const release = {
85
- latest,
86
- current,
87
- behind: compareVersions(current, latest) < 0,
88
- command: UPGRADE_COMMAND,
89
- checkedAt: now.toISOString(),
90
- };
91
- await ensureConfigDir(harborDir());
92
- await writeFileAtomic(cliReleaseFile(), `${JSON.stringify(release, null, 2)}\n`);
93
- return { kind: 'checked', release };
94
- }
95
- export function describeRelease(check) {
96
- if (check.kind === 'unreachable' || check.kind === 'unknown-version')
97
- return null;
98
- const { release } = check;
99
- return release.behind
100
- ? `${release.latest} available (this machine runs ${release.current})`
101
- : null;
102
- }
@@ -1,23 +0,0 @@
1
- import type { Endpoints } from '../endpoints.js';
2
- export declare class NotSignedInError extends Error {
3
- constructor();
4
- }
5
- export declare class NoWorkspaceChosenError extends Error {
6
- constructor();
7
- }
8
- export declare class NotARepoError extends Error {
9
- constructor(cwd: string);
10
- }
11
- export interface BindOptions {
12
- readonly cwd: string;
13
- readonly endpoints: Endpoints;
14
- readonly project?: string;
15
- }
16
- export interface BindResult {
17
- readonly originUrl: string;
18
- readonly projectName: string | null;
19
- readonly alreadyBound: boolean;
20
- readonly originCount: number;
21
- }
22
- export declare function runBind(opts: BindOptions): Promise<BindResult>;
23
- export declare function renderBind(result: BindResult): string;
@@ -1,86 +0,0 @@
1
- import { createRepoBinding, listProjects, listRepoBindings, } from '../api.js';
2
- import { isPendingWorkspace, readAuth } from '../auth/store.js';
3
- import { installProjectOrigin } from '../providers/claude.js';
4
- import { gitRoot, repoOrigin } from '../origin.js';
5
- import { noteCheckout } from '../repos.js';
6
- import { updateSyncedConfig } from '../state.js';
7
- export class NotSignedInError extends Error {
8
- constructor() {
9
- super('Not signed in. Run `harbor init` first.');
10
- this.name = 'NotSignedInError';
11
- }
12
- }
13
- export class NoWorkspaceChosenError extends Error {
14
- constructor() {
15
- super('Signed in, but this machine has no workspace yet. Run `harbor login` to ' +
16
- 'finish choosing one — it will not need the browser again.');
17
- this.name = 'NoWorkspaceChosenError';
18
- }
19
- }
20
- export class NotARepoError extends Error {
21
- constructor(cwd) {
22
- super(`${cwd} has no git remote named "origin", so there is nothing to bind.`);
23
- this.name = 'NotARepoError';
24
- }
25
- }
26
- export async function runBind(opts) {
27
- const auth = await readAuth();
28
- if (!auth)
29
- throw new NotSignedInError();
30
- if (isPendingWorkspace(auth))
31
- throw new NoWorkspaceChosenError();
32
- const origin = repoOrigin(opts.cwd);
33
- if (origin === '')
34
- throw new NotARepoError(opts.cwd);
35
- const existing = await listRepoBindings(opts.endpoints.apiUrl, auth.runnerToken, auth.workspaceId);
36
- const already = existing.find((b) => b.originUrl === origin) ?? null;
37
- let projectId;
38
- let projectName = already?.projectName ?? null;
39
- if (opts.project) {
40
- const projects = await listProjects(opts.endpoints.apiUrl, auth.runnerToken, auth.workspaceId);
41
- const match = projects.find((p) => p.id === opts.project) ??
42
- projects.find((p) => p.name.toLowerCase() === opts.project.toLowerCase());
43
- if (!match) {
44
- throw new Error(`No project named "${opts.project}". Available: ${projects.map((p) => p.name).join(', ') || '(none)'}`);
45
- }
46
- projectId = match.id;
47
- projectName = match.name;
48
- }
49
- let binding = already;
50
- if (!already) {
51
- binding = await createRepoBinding(opts.endpoints.apiUrl, auth.runnerToken, auth.workspaceId, { originUrl: origin, ...(projectId ? { projectId } : {}) });
52
- projectName = binding.projectName ?? projectName;
53
- }
54
- const refreshed = await listRepoBindings(opts.endpoints.apiUrl, auth.runnerToken, auth.workspaceId);
55
- const config = await updateSyncedConfig((current) => ({
56
- ...current,
57
- apiBase: opts.endpoints.apiUrl,
58
- origins: refreshed.map((b) => b.originUrl),
59
- }));
60
- const root = gitRoot(opts.cwd);
61
- if (root !== null)
62
- await noteCheckout(origin, root);
63
- if (root !== null) {
64
- try {
65
- await installProjectOrigin({ repoPath: root, originUrl: origin });
66
- }
67
- catch {
68
- }
69
- }
70
- return {
71
- originUrl: origin,
72
- projectName,
73
- alreadyBound: already !== null,
74
- originCount: config.origins.length,
75
- };
76
- }
77
- export function renderBind(result) {
78
- const scope = result.projectName
79
- ? `project "${result.projectName}"`
80
- : 'the workspace (no project)';
81
- const verb = result.alreadyBound ? 'Already bound' : 'Bound';
82
- return [
83
- `${verb}: ${result.originUrl} → ${scope}`,
84
- `${result.originCount} repo${result.originCount === 1 ? '' : 's'} in the local allowlist.`,
85
- ].join('\n');
86
- }
@@ -1,12 +0,0 @@
1
- import { type ContextBudget } from '../context-budget.js';
2
- export interface ContextOptions {
3
- readonly cwd: string;
4
- readonly verbose?: boolean;
5
- }
6
- export interface ContextResult extends ContextBudget {
7
- readonly repoPath: string | null;
8
- readonly verbose: boolean;
9
- }
10
- export declare function runContext(opts: ContextOptions): Promise<ContextResult>;
11
- export declare function renderContext(result: ContextResult): string;
12
- export declare function contextToJson(result: ContextResult): unknown;
@@ -1,80 +0,0 @@
1
- import { gitRoot } from '../origin.js';
2
- import { measureContext, } from '../context-budget.js';
3
- export async function runContext(opts) {
4
- const repoPath = gitRoot(opts.cwd);
5
- const budget = await measureContext({ repoPath });
6
- return { ...budget, repoPath, verbose: opts.verbose === true };
7
- }
8
- function figure(tokens) {
9
- return tokens.toLocaleString('en-US');
10
- }
11
- const OWNER_LABEL = {
12
- harbor: 'harbor',
13
- 'other-tool': 'other tool',
14
- nobody: 'unowned',
15
- };
16
- function renderLine(line, width) {
17
- const count = line.count === null ? '' : ` (${line.count})`;
18
- const label = `${line.label}${count}`.padEnd(width);
19
- const cost = line.tokens === null ? '—'.padStart(9) : `~${figure(line.tokens)}`.padStart(9);
20
- const tail = line.unmeasured ?? line.note ?? '';
21
- return ` ${label} ${cost}${tail ? ` ${tail}` : ''}`;
22
- }
23
- export function renderContext(result) {
24
- const width = Math.max(...result.lines.map((line) => `${line.label}${line.count === null ? '' : ` (${line.count})`}`.length));
25
- const out = [];
26
- const header = result.unmeasuredLines > 0
27
- ? `at least ~${figure(result.measuredTokens)} tok / turn`
28
- : `~${figure(result.measuredTokens)} tok / turn`;
29
- out.push(` always-on budget on this machine ${header}`);
30
- for (const line of result.lines)
31
- out.push(renderLine(line, width));
32
- const { harbor, nobody } = result.ownership;
33
- const other = result.ownership['other-tool'];
34
- const indexed = result.skills.length - result.undiscoverable;
35
- out.push('');
36
- out.push(` managed by harbor: ${harbor} of ${indexed} · unowned: ${nobody} · other tools: ${other}`);
37
- if (result.undiscoverable > 0) {
38
- out.push(` ${result.undiscoverable} more file${result.undiscoverable === 1 ? '' : 's'} in a skills root carry no description — write one, or they are shelf-ware`);
39
- }
40
- if (result.repoPath === null) {
41
- out.push(' (home only — run this inside a repository to price its lane too)');
42
- }
43
- if (result.verbose && result.skills.length > 0) {
44
- out.push('');
45
- const nameWidth = Math.max(...result.skills.map((s) => s.name.length));
46
- for (const skill of sortedSkills(result.skills)) {
47
- const copies = skill.copies > 1 ? ` ×${skill.copies} roots` : '';
48
- out.push(` ${skill.name.padEnd(nameWidth)} ${`~${figure(skill.tokens)}`.padStart(7)} ${OWNER_LABEL[skill.owner]}${skill.undiscoverable ? ' no description' : ''}${copies}`);
49
- }
50
- }
51
- return out.join('\n');
52
- }
53
- function sortedSkills(skills) {
54
- return [...skills].sort((a, b) => b.tokens - a.tokens || a.name.localeCompare(b.name));
55
- }
56
- export function contextToJson(result) {
57
- return {
58
- measuredTokens: result.measuredTokens,
59
- unmeasuredLines: result.unmeasuredLines,
60
- repoPath: result.repoPath,
61
- ownership: result.ownership,
62
- undiscoverable: result.undiscoverable,
63
- lines: result.lines.map((line) => ({
64
- id: line.id,
65
- label: line.label,
66
- count: line.count,
67
- tokens: line.tokens,
68
- ...(line.unmeasured ? { unmeasured: line.unmeasured } : {}),
69
- ...(line.note ? { note: line.note } : {}),
70
- })),
71
- skills: result.skills.map((skill) => ({
72
- name: skill.name,
73
- owner: skill.owner,
74
- tokens: skill.tokens,
75
- undiscoverable: skill.undiscoverable,
76
- copies: skill.copies,
77
- path: skill.path,
78
- })),
79
- };
80
- }
@@ -1,39 +0,0 @@
1
- import type { Endpoints } from '../endpoints.js';
2
- import { type InstallOutcome } from '../daemon/install.js';
3
- import { type DaemonVerdict } from '../daemon/state.js';
4
- import { type DaemonTickReport } from '../daemon/tick.js';
5
- export interface DaemonOptions {
6
- readonly endpoints: Endpoints;
7
- readonly once?: boolean;
8
- readonly install?: boolean;
9
- readonly uninstall?: boolean;
10
- readonly status?: boolean;
11
- readonly env?: NodeJS.ProcessEnv;
12
- readonly fetchImpl?: typeof fetch;
13
- readonly home?: string;
14
- readonly signal?: AbortSignal;
15
- }
16
- export type DaemonResult = {
17
- readonly mode: 'once';
18
- readonly report: DaemonTickReport;
19
- } | {
20
- readonly mode: 'skipped';
21
- readonly detail: string;
22
- } | {
23
- readonly mode: 'status';
24
- readonly verdict: DaemonVerdict;
25
- readonly units: readonly string[];
26
- } | {
27
- readonly mode: 'install';
28
- readonly outcome: InstallOutcome;
29
- } | {
30
- readonly mode: 'uninstall';
31
- readonly outcome: InstallOutcome;
32
- } | {
33
- readonly mode: 'ran';
34
- readonly ticks: number;
35
- };
36
- export declare function runDaemon(opts: DaemonOptions): Promise<DaemonResult>;
37
- export declare function renderTick(report: DaemonTickReport): string;
38
- export declare function renderDaemon(result: DaemonResult): string;
39
- export declare function exitCodeForDaemon(result: DaemonResult): number;
@@ -1,140 +0,0 @@
1
- import { daemonProgram, installDaemon, installPlan, installedFiles, uninstallDaemon, } from '../daemon/install.js';
2
- import { runDaemonLoop, TICK_INTERVAL_MS } from '../daemon/loop.js';
3
- import { daemonVerdict, readDaemonHeartbeat, recordTick, } from '../daemon/state.js';
4
- import { runDaemonTick, summarise } from '../daemon/tick.js';
5
- import { harborDir } from '../paths.js';
6
- export async function runDaemon(opts) {
7
- if (opts.status === true) {
8
- const plan = installPlan({
9
- program: safeProgram(),
10
- harborHome: harborDir(),
11
- });
12
- return {
13
- mode: 'status',
14
- verdict: daemonVerdict(await readDaemonHeartbeat()),
15
- units: await installedFiles(plan),
16
- };
17
- }
18
- if (opts.install === true || opts.uninstall === true) {
19
- const plan = installPlan({
20
- program: daemonProgram(),
21
- harborHome: harborDir(),
22
- });
23
- return opts.install === true
24
- ? { mode: 'install', outcome: await installDaemon({ plan }) }
25
- : { mode: 'uninstall', outcome: await uninstallDaemon({ plan }) };
26
- }
27
- if (opts.once === true) {
28
- const outcome = await runDaemonTick({
29
- endpoints: opts.endpoints,
30
- reason: 'once',
31
- ...(opts.env ? { env: opts.env } : {}),
32
- ...(opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}),
33
- ...(opts.home === undefined ? {} : { home: opts.home }),
34
- });
35
- if (outcome.kind === 'contended') {
36
- return { mode: 'skipped', detail: outcome.detail };
37
- }
38
- const report = outcome.report;
39
- await recordTick(summarise(report), {
40
- mode: 'once',
41
- startedBy: 'cli',
42
- startedAt: report.startedAt,
43
- pid: process.pid,
44
- intervalMs: TICK_INTERVAL_MS,
45
- });
46
- return { mode: 'once', report };
47
- }
48
- let ticks = 0;
49
- await runDaemonLoop({
50
- endpoints: opts.endpoints,
51
- ...(opts.env ? { env: opts.env } : {}),
52
- ...(opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}),
53
- ...(opts.home === undefined ? {} : { home: opts.home }),
54
- ...(opts.signal ? { signal: opts.signal } : {}),
55
- onTick: (report) => {
56
- ticks += 1;
57
- process.stdout.write(`${renderTick(report)}\n`);
58
- },
59
- onSkipped: (detail) => process.stdout.write(`· ${detail}\n`),
60
- });
61
- return { mode: 'ran', ticks };
62
- }
63
- function safeProgram() {
64
- try {
65
- return daemonProgram();
66
- }
67
- catch {
68
- return { interpreter: process.execPath, script: '' };
69
- }
70
- }
71
- const GLYPH = { ok: '✓', skipped: '·', failed: '✗' };
72
- export function renderTick(report) {
73
- const lines = [
74
- `${GLYPH[report.credential.kind]} credential ${report.credential.detail}`,
75
- `${GLYPH[report.env.kind]} your env ${report.env.detail}`,
76
- ];
77
- for (const repo of report.repos) {
78
- lines.push(`${GLYPH[repo.outcome.kind]} ${repo.path} ${repo.outcome.detail}`);
79
- }
80
- lines.push(`${GLYPH[report.report.kind]} report ${report.report.detail}`);
81
- return lines.join('\n');
82
- }
83
- export function renderDaemon(result) {
84
- switch (result.mode) {
85
- case 'once':
86
- return renderTick(result.report);
87
- case 'skipped':
88
- return `· ${result.detail}`;
89
- case 'status': {
90
- const glyph = result.verdict.kind === 'ok'
91
- ? '✓'
92
- : result.verdict.kind === 'degraded' || result.verdict.kind === 'idle'
93
- ? '⚠'
94
- : '✗';
95
- const head = `${glyph} ${result.verdict.detail}`;
96
- const lines = [head];
97
- if ('fix' in result.verdict)
98
- lines.push(` ↳ ${result.verdict.fix}`);
99
- lines.push(result.units.length === 0
100
- ? ' Not scheduled. `harbor daemon --install` sets it up.'
101
- : ` Scheduled by ${result.units.join(', ')}`);
102
- return lines.join('\n');
103
- }
104
- case 'install':
105
- case 'uninstall': {
106
- const lines = [result.outcome.detail];
107
- for (const file of result.outcome.files)
108
- lines.push(` ${file}`);
109
- for (const argv of result.outcome.manual)
110
- lines.push(` ${argv.join(' ')}`);
111
- return lines.join('\n');
112
- }
113
- case 'ran':
114
- return `Stopped after ${result.ticks} tick${result.ticks === 1 ? '' : 's'}.`;
115
- }
116
- }
117
- export function exitCodeForDaemon(result) {
118
- if (result.mode === 'skipped')
119
- return 0;
120
- if (result.mode === 'once') {
121
- const failed = result.report.credential.kind === 'failed' ||
122
- result.report.env.kind === 'failed' ||
123
- result.report.report.kind === 'failed' ||
124
- result.report.repos.some((r) => r.outcome.kind === 'failed');
125
- return failed ? 1 : 0;
126
- }
127
- if (result.mode === 'status') {
128
- return result.verdict.kind === 'ok' ||
129
- result.verdict.kind === 'degraded' ||
130
- result.verdict.kind === 'idle'
131
- ? 0
132
- : 1;
133
- }
134
- if (result.mode === 'install' || result.mode === 'uninstall') {
135
- return result.outcome.kind === 'partial' || result.outcome.kind === 'unsupported'
136
- ? 1
137
- : 0;
138
- }
139
- return 0;
140
- }