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
@@ -1,231 +0,0 @@
1
- import { refreshRunnerToken } from '../api.js';
2
- import { refreshCliRelease } from '../cli-release.js';
3
- import { isExpired, jwtExpiry, readAuth, resolveCredentials, writeAuth, } from '../auth/store.js';
4
- import { syncEnvPlane } from '../env-sync.js';
5
- import { syncRepoPlane } from '../repo-sync.js';
6
- import { readRepoRegistry } from '../repos.js';
7
- import { EMPTY_PAUSE, readSyncedConfig, resolveArms, } from '../state.js';
8
- import { buildSyncReport, postSyncReport } from '../sync-report.js';
9
- import { withTickLease } from './state.js';
10
- export const REFRESH_WHEN_LESS_THAN_MS = 48 * 60 * 60 * 1000;
11
- const ok = (detail) => ({ kind: 'ok', detail });
12
- const skipped = (detail) => ({ kind: 'skipped', detail });
13
- const failed = (detail) => ({ kind: 'failed', detail });
14
- const message = (err) => err instanceof Error ? err.message : String(err);
15
- const countSynced = (rows) => rows.filter((row) => row.status === 'synced').length;
16
- async function tickCredential(endpoints, source, fetchImpl, now) {
17
- if (source === 'env') {
18
- return skipped('Using HARBOR_TOKEN from the environment — not ours to rotate.');
19
- }
20
- const auth = await readAuth();
21
- if (!auth)
22
- return skipped('Not signed in.');
23
- if (isExpired(auth.runnerToken, now)) {
24
- return failed("This machine's credential has expired. Run `harbor login` again.");
25
- }
26
- const expiry = jwtExpiry(auth.runnerToken);
27
- const remaining = expiry === null ? null : expiry.getTime() - now.getTime();
28
- if (remaining !== null && remaining > REFRESH_WHEN_LESS_THAN_MS) {
29
- return skipped(`Credential good for ${Math.floor(remaining / 3_600_000)}h.`);
30
- }
31
- try {
32
- const rotated = await refreshRunnerToken(auth.apiUrl || endpoints.apiUrl, auth.runnerToken, fetchImpl);
33
- const current = (await readAuth()) ?? auth;
34
- await writeAuth({ ...current, runnerToken: rotated });
35
- return ok('Credential rotated.');
36
- }
37
- catch (err) {
38
- return failed(message(err));
39
- }
40
- }
41
- async function tickEnv(input) {
42
- try {
43
- const result = await syncEnvPlane(input);
44
- if (result.skills.kind === 'error' || result.documents.kind === 'error') {
45
- return { arm: failed('Could not fetch this machine’s own bundle.') };
46
- }
47
- if (result.skills.kind === 'unsupported') {
48
- return { arm: skipped('This backend does not serve a personal bundle yet.') };
49
- }
50
- const skills = countSynced(result.skills.summary.results);
51
- const documents = result.documents.kind === 'synced' && result.documents.outcome.kind === 'applied'
52
- ? countSynced(result.documents.outcome.documents)
53
- : 0;
54
- return {
55
- arm: ok(`${skills} skills · ${documents} documents.`),
56
- ...(result.bundleHash === undefined ? {} : { bundleHash: result.bundleHash }),
57
- };
58
- }
59
- catch (err) {
60
- return { arm: failed(message(err)) };
61
- }
62
- }
63
- function describeRepo(result) {
64
- if (result.kind === 'error')
65
- return failed(result.message);
66
- if (result.kind === 'sync_disabled') {
67
- return skipped(result.reason === 'workspace'
68
- ? 'Sync is off for this workspace — an admin turns it on in the web app.'
69
- : 'Sync is off for this repository.');
70
- }
71
- const documents = result.documents.kind === 'applied' ? countSynced(result.documents.documents) : 0;
72
- return ok(`${countSynced(result.skills.results)} skills · ${countSynced(result.memories.results)} memories · ${documents} documents.`);
73
- }
74
- async function tickRepos(input) {
75
- const entries = await readRepoRegistry();
76
- const out = [];
77
- for (const entry of entries) {
78
- const base = { path: entry.path, originUrl: entry.originUrl };
79
- if (!input.allowlist.has(entry.originUrl)) {
80
- out.push({ ...base, outcome: skipped("Not in this machine's allowlist yet.") });
81
- continue;
82
- }
83
- if (!resolveArms(input.pause, entry.originUrl).recall) {
84
- out.push({ ...base, outcome: skipped('Paused.') });
85
- continue;
86
- }
87
- try {
88
- const result = await syncRepoPlane({
89
- apiBase: input.apiBase,
90
- workspaceId: input.workspaceId,
91
- bearerToken: input.bearerToken,
92
- repoPath: entry.path,
93
- originUrl: entry.originUrl,
94
- ...(input.fetchImpl ? { fetchImpl: input.fetchImpl } : {}),
95
- ...(input.home === undefined ? {} : { home: input.home }),
96
- });
97
- out.push({ ...base, outcome: describeRepo(result) });
98
- }
99
- catch (err) {
100
- out.push({ ...base, outcome: failed(message(err)) });
101
- }
102
- }
103
- return out;
104
- }
105
- async function tickReport(input) {
106
- try {
107
- const report = await buildSyncReport({
108
- ...(input.home === undefined ? {} : { home: input.home }),
109
- ...(input.bundleHash === undefined ? {} : { bundleHash: input.bundleHash }),
110
- });
111
- const posted = await postSyncReport({
112
- apiBase: input.apiBase,
113
- workspaceId: input.workspaceId,
114
- bearerToken: input.bearerToken,
115
- report,
116
- ...(input.fetchImpl ? { fetchImpl: input.fetchImpl } : {}),
117
- });
118
- if (posted.kind === 'ok')
119
- return ok(`${report.files.length} files reported.`);
120
- return posted.kind === 'unsupported'
121
- ? skipped('This backend does not accept machine reports yet.')
122
- : failed(posted.message);
123
- }
124
- catch (err) {
125
- return failed(message(err));
126
- }
127
- }
128
- export async function runDaemonTick(opts) {
129
- const lease = await withTickLease(() => tickPass(opts), opts.lockWaitMs === undefined ? {} : { waitMs: opts.lockWaitMs });
130
- return lease.kind === 'ran' ? { kind: 'ran', report: lease.value } : lease;
131
- }
132
- async function tickPass(opts) {
133
- const clock = opts.now ?? (() => new Date());
134
- const startedAt = clock().toISOString();
135
- const finish = (parts) => ({
136
- startedAt,
137
- finishedAt: clock().toISOString(),
138
- reason: opts.reason,
139
- ...parts,
140
- });
141
- let credentials;
142
- try {
143
- credentials = await resolveCredentials(opts.env);
144
- }
145
- catch (err) {
146
- const none = skipped('No usable credential.');
147
- return finish({
148
- credential: failed(message(err)),
149
- env: none,
150
- repos: [],
151
- report: none,
152
- });
153
- }
154
- if (!credentials) {
155
- const nope = skipped('Not signed in — run `harbor login`.');
156
- return finish({ credential: nope, env: nope, repos: [], report: nope });
157
- }
158
- const credential = await tickCredential(opts.endpoints, credentials.source, opts.fetchImpl, clock());
159
- const stored = credentials.source === 'file' ? await readAuth() : null;
160
- const bearer = stored?.runnerToken ?? credentials.runnerToken;
161
- const apiBase = stored?.apiUrl ?? opts.endpoints.apiUrl;
162
- const config = await readSyncedConfig();
163
- const pause = config?.pause ?? EMPTY_PAUSE;
164
- const passthrough = {
165
- ...(opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}),
166
- ...(opts.home === undefined ? {} : { home: opts.home }),
167
- };
168
- const env = pause.global === 'off'
169
- ? { arm: skipped('Paused globally — `harbor on` resumes.') }
170
- : await tickEnv({
171
- apiBase,
172
- workspaceId: credentials.workspaceId,
173
- bearerToken: bearer,
174
- ...passthrough,
175
- });
176
- const repos = await tickRepos({
177
- apiBase,
178
- workspaceId: credentials.workspaceId,
179
- bearerToken: bearer,
180
- allowlist: new Set(config?.origins ?? []),
181
- pause,
182
- ...passthrough,
183
- });
184
- const report = await tickReport({
185
- apiBase,
186
- workspaceId: credentials.workspaceId,
187
- bearerToken: bearer,
188
- ...(env.bundleHash === undefined ? {} : { bundleHash: env.bundleHash }),
189
- ...passthrough,
190
- });
191
- const release = await tickRelease(passthrough.fetchImpl, clock());
192
- return finish({
193
- credential,
194
- env: env.arm,
195
- repos,
196
- report,
197
- release,
198
- ...(env.bundleHash === undefined ? {} : { bundleHash: env.bundleHash }),
199
- });
200
- }
201
- async function tickRelease(fetchImpl, now) {
202
- try {
203
- const check = await refreshCliRelease({ now, ...(fetchImpl ? { fetchImpl } : {}) });
204
- switch (check.kind) {
205
- case 'checked':
206
- case 'cached':
207
- return ok(check.release.behind
208
- ? `${check.release.latest} available - this machine runs ${check.release.current}.`
209
- : `Up to date (${check.release.current}).`);
210
- case 'unknown-version':
211
- return skipped('This build cannot read its own version.');
212
- default:
213
- return skipped('Could not reach the npm registry.');
214
- }
215
- }
216
- catch (err) {
217
- return failed(message(err));
218
- }
219
- }
220
- export function summarise(report) {
221
- return {
222
- at: report.finishedAt,
223
- reason: report.reason,
224
- durationMs: new Date(report.finishedAt).getTime() - new Date(report.startedAt).getTime(),
225
- credential: report.credential,
226
- env: report.env,
227
- repos: report.repos.length,
228
- reposFailed: report.repos.filter((r) => r.outcome.kind === 'failed').length,
229
- report: report.report,
230
- };
231
- }
@@ -1,21 +0,0 @@
1
- import type { AgentPresence } from '../providers/detect.js';
2
- import { type DocumentPayload, type DocumentResult, type DocumentRootSummary } from './write.js';
3
- import { type PointerResult } from './pointer.js';
4
- export interface ApplyDocumentsOptions {
5
- readonly workspaceId?: string;
6
- }
7
- export type ApplyDocumentsOutcome = {
8
- readonly kind: 'unsupported';
9
- } | {
10
- readonly kind: 'applied';
11
- readonly dir: string;
12
- readonly documents: readonly DocumentResult[];
13
- readonly pointer: readonly PointerResult[];
14
- readonly reclaimed: boolean;
15
- readonly roots?: readonly DocumentRootSummary[];
16
- };
17
- export declare function applyDocumentsToRepo(repoPath: string, documents: readonly DocumentPayload[] | undefined, options?: ApplyDocumentsOptions): Promise<ApplyDocumentsOutcome>;
18
- export declare function applyDocumentsToEnv(documents: readonly DocumentPayload[] | undefined, options?: ApplyDocumentsOptions & {
19
- readonly home?: string;
20
- readonly agents?: AgentPresence;
21
- }): Promise<ApplyDocumentsOutcome>;
@@ -1,58 +0,0 @@
1
- import { homedir } from 'node:os';
2
- import { join } from 'node:path';
3
- import { withFileLock } from '../lock.js';
4
- import { DOCS_DIR } from './manifest.js';
5
- import { syncDocumentFilesToEnv, syncDocumentFilesToRepo, } from './write.js';
6
- import { removeEnvPointerLines, removeRepoPointerLines, syncPointerLineToEnv, syncPointerLineToRepo, } from './pointer.js';
7
- export async function applyDocumentsToRepo(repoPath, documents, options = {}) {
8
- if (documents === undefined)
9
- return { kind: 'unsupported' };
10
- return withFileLock(join(repoPath, DOCS_DIR), async () => {
11
- const synced = await syncDocumentFilesToRepo(repoPath, documents, {
12
- ...(options.workspaceId === undefined ? {} : { workspaceId: options.workspaceId }),
13
- });
14
- let pointer = [];
15
- if (documents.length > 0) {
16
- pointer = (await syncPointerLineToRepo(repoPath, true)).results;
17
- }
18
- else if (synced.reclaimed) {
19
- pointer = (await removeRepoPointerLines(repoPath)).results;
20
- }
21
- return {
22
- kind: 'applied',
23
- dir: synced.dir,
24
- documents: synced.results,
25
- pointer,
26
- reclaimed: synced.reclaimed,
27
- };
28
- });
29
- }
30
- export async function applyDocumentsToEnv(documents, options = {}) {
31
- if (documents === undefined)
32
- return { kind: 'unsupported' };
33
- const home = options.home ?? homedir();
34
- return withFileLock(join(home, DOCS_DIR), async () => {
35
- const synced = await syncDocumentFilesToEnv(documents, {
36
- home,
37
- ...(options.agents === undefined ? {} : { agents: options.agents }),
38
- ...(options.workspaceId === undefined
39
- ? {}
40
- : { workspaceId: options.workspaceId }),
41
- });
42
- let pointer = [];
43
- if (documents.length > 0) {
44
- pointer = (await syncPointerLineToEnv(synced.roots, true, { home })).results;
45
- }
46
- else if (synced.reclaimed) {
47
- pointer = (await removeEnvPointerLines(synced.roots, { home })).results;
48
- }
49
- return {
50
- kind: 'applied',
51
- dir: synced.dir,
52
- documents: synced.results,
53
- roots: synced.roots,
54
- pointer,
55
- reclaimed: synced.reclaimed,
56
- };
57
- });
58
- }
@@ -1,41 +0,0 @@
1
- export interface ContextBundlePayload {
2
- readonly id: string;
3
- readonly name: string;
4
- readonly content: string;
5
- }
6
- export interface ContextBundleSkill extends ContextBundlePayload {
7
- readonly path?: string;
8
- }
9
- export interface ContextBundleDocument {
10
- readonly id: string;
11
- readonly name: string;
12
- readonly path: string;
13
- readonly content: string;
14
- }
15
- export interface ContextBundle {
16
- readonly contentHash: string;
17
- readonly skills: ReadonlyArray<ContextBundleSkill>;
18
- readonly memories: ReadonlyArray<ContextBundlePayload>;
19
- readonly documents?: ReadonlyArray<ContextBundleDocument>;
20
- readonly lastUpdated: string;
21
- }
22
- export type FetchBundleResult = {
23
- readonly kind: 'ok';
24
- readonly bundle: ContextBundle;
25
- } | {
26
- readonly kind: 'sync_disabled';
27
- readonly reason: 'workspace' | 'admin';
28
- } | {
29
- readonly kind: 'error';
30
- readonly status: number | 'network';
31
- readonly message: string;
32
- };
33
- export interface FetchBundleInput {
34
- readonly apiBase: string;
35
- readonly workspaceId: string;
36
- readonly originUrl: string;
37
- readonly bearerToken: string;
38
- readonly fetchImpl?: typeof fetch;
39
- }
40
- export declare function contextBundleUrl(apiBase: string, workspaceId: string, originUrl: string): string;
41
- export declare function fetchContextBundle(input: FetchBundleInput): Promise<FetchBundleResult>;
@@ -1,43 +0,0 @@
1
- import { ApiError, apiRequest } from '../api.js';
2
- function stripTrailingSlash(s) {
3
- return s.endsWith('/') ? s.slice(0, -1) : s;
4
- }
5
- export function contextBundleUrl(apiBase, workspaceId, originUrl) {
6
- return (`${stripTrailingSlash(apiBase)}/workspaces/${encodeURIComponent(workspaceId)}` +
7
- `/context/repo-bindings/context-bundle?originUrl=${encodeURIComponent(originUrl)}`);
8
- }
9
- export async function fetchContextBundle(input) {
10
- const url = contextBundleUrl(input.apiBase, input.workspaceId, input.originUrl);
11
- try {
12
- const body = await apiRequest(url, {
13
- bearer: input.bearerToken,
14
- fetchImpl: input.fetchImpl,
15
- });
16
- const data = body?.data;
17
- if (!data) {
18
- return { kind: 'error', status: 200, message: 'missing data field' };
19
- }
20
- return { kind: 'ok', bundle: data };
21
- }
22
- catch (err) {
23
- if (!(err instanceof ApiError)) {
24
- return {
25
- kind: 'error',
26
- status: 'network',
27
- message: err instanceof Error ? err.message : String(err),
28
- };
29
- }
30
- if (err.status === 403) {
31
- const reason = err.body?.reason;
32
- return {
33
- kind: 'sync_disabled',
34
- reason: reason === 'workspace' ? 'workspace' : 'admin',
35
- };
36
- }
37
- return {
38
- kind: 'error',
39
- status: err.status ?? 'network',
40
- message: err.message,
41
- };
42
- }
43
- }
@@ -1,12 +0,0 @@
1
- export type DocumentState = 'ours' | 'forked' | 'missing' | 'unverifiable';
2
- export interface DocumentStatus {
3
- readonly file: string;
4
- readonly state: DocumentState;
5
- }
6
- export interface RepoDocuments {
7
- readonly dir: string;
8
- readonly managed: boolean;
9
- readonly documents: readonly DocumentStatus[];
10
- }
11
- export declare function inspectRepoDocuments(repoRoot: string): Promise<RepoDocuments>;
12
- export declare function inState(docs: RepoDocuments, state: DocumentState): readonly string[];
@@ -1,33 +0,0 @@
1
- import { promises as fsp } from 'node:fs';
2
- import { join } from 'node:path';
3
- import { DOCS_DIR, MANIFEST_FILE, hashDocument, parseManifest, } from './manifest.js';
4
- export async function inspectRepoDocuments(repoRoot) {
5
- const dir = join(repoRoot, DOCS_DIR);
6
- const raw = await fsp
7
- .readFile(join(dir, MANIFEST_FILE), 'utf8')
8
- .catch(() => null);
9
- if (raw === null)
10
- return { dir, managed: false, documents: [] };
11
- const { files, hashes } = parseManifest(raw);
12
- const documents = [];
13
- for (const file of files) {
14
- const content = await fsp.readFile(join(dir, file), 'utf8').catch(() => null);
15
- if (content === null) {
16
- documents.push({ file, state: 'missing' });
17
- continue;
18
- }
19
- const recorded = hashes.get(file);
20
- if (recorded === undefined) {
21
- documents.push({ file, state: 'unverifiable' });
22
- continue;
23
- }
24
- documents.push({
25
- file,
26
- state: hashDocument(content) === recorded ? 'ours' : 'forked',
27
- });
28
- }
29
- return { dir, managed: true, documents };
30
- }
31
- export function inState(docs, state) {
32
- return docs.documents.filter((d) => d.state === state).map((d) => d.file);
33
- }
@@ -1,14 +0,0 @@
1
- export declare const DOCS_DIR = ".claude/docs";
2
- export declare const MANIFEST_FILE = ".milestone-managed";
3
- export interface ManifestEntry {
4
- readonly file: string;
5
- readonly sha256: string | null;
6
- }
7
- export interface ParsedManifest {
8
- readonly files: readonly string[];
9
- readonly hashes: ReadonlyMap<string, string>;
10
- readonly workspaceId: string | null;
11
- }
12
- export declare function hashDocument(content: string): string;
13
- export declare function parseManifest(raw: string): ParsedManifest;
14
- export declare function renderManifest(entries: readonly ManifestEntry[], workspaceId?: string | null): string;
@@ -1,52 +0,0 @@
1
- import { createHash } from 'node:crypto';
2
- export const DOCS_DIR = '.claude/docs';
3
- export const MANIFEST_FILE = '.milestone-managed';
4
- const HEADER = [
5
- '# Files in this directory that harbor maintains.',
6
- '# Anything not listed here was written by a human and is never touched.',
7
- ];
8
- const HASH_PREFIX = '# sha256 ';
9
- const HASH_LINE = /^#\s*sha256\s+([0-9a-f]{64})\s+(.+)$/;
10
- const WORKSPACE_PREFIX = '# workspace: ';
11
- export function hashDocument(content) {
12
- return createHash('sha256').update(content, 'utf8').digest('hex');
13
- }
14
- export function parseManifest(raw) {
15
- const files = [];
16
- const hashes = new Map();
17
- let workspaceId = null;
18
- for (const line of raw.split('\n')) {
19
- const trimmed = line.trim();
20
- if (trimmed.length === 0)
21
- continue;
22
- if (trimmed.startsWith('#')) {
23
- const match = trimmed.match(HASH_LINE);
24
- if (match) {
25
- hashes.set(match[2].trim(), match[1]);
26
- }
27
- else if (trimmed.startsWith(WORKSPACE_PREFIX)) {
28
- workspaceId = trimmed.slice(WORKSPACE_PREFIX.length).trim() || null;
29
- }
30
- continue;
31
- }
32
- files.push(trimmed);
33
- }
34
- for (const file of hashes.keys()) {
35
- if (!files.includes(file))
36
- hashes.delete(file);
37
- }
38
- return { files, hashes, workspaceId };
39
- }
40
- export function renderManifest(entries, workspaceId) {
41
- const names = entries.map((entry) => entry.file);
42
- const digests = entries
43
- .filter((entry) => entry.sha256 !== null)
44
- .map((entry) => `${HASH_PREFIX}${entry.sha256} ${entry.file}`);
45
- return [
46
- ...HEADER,
47
- ...(workspaceId ? [`${WORKSPACE_PREFIX}${workspaceId}`] : []),
48
- ...names,
49
- ...digests,
50
- '',
51
- ].join('\n');
52
- }
@@ -1,32 +0,0 @@
1
- import type { AgentId } from '../providers/detect.js';
2
- export declare const POINTER_LINE = "Team documents are in `.claude/docs/` \u2014 read the relevant one when it applies.";
3
- export type PointerResult = {
4
- readonly file: string;
5
- readonly status: 'added';
6
- } | {
7
- readonly file: string;
8
- readonly status: 'present';
9
- } | {
10
- readonly file: string;
11
- readonly status: 'created';
12
- } | {
13
- readonly file: string;
14
- readonly status: 'removed';
15
- };
16
- export interface PointerSyncSummary {
17
- readonly results: readonly PointerResult[];
18
- }
19
- export declare function syncPointerLineToRepo(repoPath: string, hasDocuments: boolean): Promise<PointerSyncSummary>;
20
- export declare function removeRepoPointerLines(repoPath: string): Promise<PointerSyncSummary>;
21
- export declare const HOME_AGENT_FILE: Record<AgentId, string | null>;
22
- export declare function homePointerLine(docsDir: string, home?: string): string;
23
- export interface PointerRoot {
24
- readonly agent: AgentId;
25
- readonly dir: string;
26
- }
27
- export declare function syncPointerLineToEnv(roots: readonly PointerRoot[], hasDocuments: boolean, options?: {
28
- readonly home?: string;
29
- }): Promise<PointerSyncSummary>;
30
- export declare function removeEnvPointerLines(roots: readonly PointerRoot[], options?: {
31
- readonly home?: string;
32
- }): Promise<PointerSyncSummary>;
@@ -1,135 +0,0 @@
1
- import { existsSync, promises as fsp } from 'node:fs';
2
- import { homedir } from 'node:os';
3
- import { join } from 'node:path';
4
- import { writeFileAtomic } from '../lock.js';
5
- import { AGENT_ROOT } from '../providers/surfaces.js';
6
- export const POINTER_LINE = 'Team documents are in `.claude/docs/` — read the relevant one when it applies.';
7
- const DOCS_PATH_TOKEN = '.claude/docs/';
8
- const AGENT_FILES = ['AGENTS.md', 'CLAUDE.md'];
9
- const NEW_FILE_BODY = `# Agent instructions\n\n${POINTER_LINE}\n`;
10
- async function readIfExists(path) {
11
- return fsp.readFile(path, 'utf8').catch(() => null);
12
- }
13
- export async function syncPointerLineToRepo(repoPath, hasDocuments) {
14
- if (!existsSync(repoPath)) {
15
- throw new Error(`Repo path does not exist: ${repoPath}`);
16
- }
17
- if (!hasDocuments)
18
- return { results: [] };
19
- const existing = AGENT_FILES.filter((name) => existsSync(join(repoPath, name)));
20
- if (existing.length === 0) {
21
- const target = join(repoPath, AGENT_FILES[0]);
22
- await writeFileAtomic(target, NEW_FILE_BODY, { mode: 'preserve' });
23
- return { results: [{ file: AGENT_FILES[0], status: 'created' }] };
24
- }
25
- const results = [];
26
- for (const name of existing) {
27
- const target = join(repoPath, name);
28
- const current = await readIfExists(target);
29
- if (current === null)
30
- continue;
31
- if (current.includes(DOCS_PATH_TOKEN)) {
32
- results.push({ file: name, status: 'present' });
33
- continue;
34
- }
35
- const separator = current.endsWith('\n\n') ? '' : current.endsWith('\n') ? '\n' : '\n\n';
36
- await writeFileAtomic(target, `${current}${separator}${POINTER_LINE}\n`, {
37
- mode: 'preserve',
38
- });
39
- results.push({ file: name, status: 'added' });
40
- }
41
- return { results };
42
- }
43
- export async function removeRepoPointerLines(repoPath) {
44
- const results = [];
45
- for (const name of AGENT_FILES) {
46
- const target = join(repoPath, name);
47
- const current = await readIfExists(target);
48
- if (current === null)
49
- continue;
50
- const lines = current.split('\n');
51
- const kept = lines.filter((line) => line.trim() !== POINTER_LINE);
52
- if (kept.length === lines.length)
53
- continue;
54
- const rebuilt = kept.join('\n').replace(/\n{3,}$/, '\n\n').replace(/\n+$/, '\n');
55
- await writeFileAtomic(target, rebuilt, { mode: 'preserve' });
56
- results.push({ file: name, status: 'removed' });
57
- }
58
- return { results };
59
- }
60
- export const HOME_AGENT_FILE = {
61
- claude: 'CLAUDE.md',
62
- codex: 'AGENTS.md',
63
- cursor: null,
64
- };
65
- const HOME_FILE_HEADING = '# Agent instructions';
66
- function displayDocsDir(docsDir, home) {
67
- const prefix = home.endsWith('/') ? home : `${home}/`;
68
- return docsDir.startsWith(prefix)
69
- ? `~/${docsDir.slice(prefix.length)}/`
70
- : `${docsDir}/`;
71
- }
72
- export function homePointerLine(docsDir, home = homedir()) {
73
- return `Your team's documents are in \`${displayDocsDir(docsDir, home)}\` — read the relevant one when it applies.`;
74
- }
75
- function homeAgentFilePath(root, home) {
76
- const name = HOME_AGENT_FILE[root.agent];
77
- return name === null ? null : join(AGENT_ROOT[root.agent](home), name);
78
- }
79
- export async function syncPointerLineToEnv(roots, hasDocuments, options = {}) {
80
- if (!hasDocuments)
81
- return { results: [] };
82
- const home = options.home ?? homedir();
83
- const results = [];
84
- for (const root of roots) {
85
- const target = homeAgentFilePath(root, home);
86
- if (target === null)
87
- continue;
88
- const line = homePointerLine(root.dir, home);
89
- const current = await readIfExists(target);
90
- if (current === null) {
91
- await fsp.mkdir(AGENT_ROOT[root.agent](home), { recursive: true });
92
- await writeFileAtomic(target, `${HOME_FILE_HEADING}\n\n${line}\n`, {
93
- mode: 'preserve',
94
- });
95
- results.push({ file: target, status: 'created' });
96
- continue;
97
- }
98
- if (current.includes(displayDocsDir(root.dir, home))) {
99
- results.push({ file: target, status: 'present' });
100
- continue;
101
- }
102
- const separator = current.endsWith('\n\n') ? '' : current.endsWith('\n') ? '\n' : '\n\n';
103
- await writeFileAtomic(target, `${current}${separator}${line}\n`, {
104
- mode: 'preserve',
105
- });
106
- results.push({ file: target, status: 'added' });
107
- }
108
- return { results };
109
- }
110
- export async function removeEnvPointerLines(roots, options = {}) {
111
- const home = options.home ?? homedir();
112
- const results = [];
113
- for (const root of roots) {
114
- const target = homeAgentFilePath(root, home);
115
- if (target === null)
116
- continue;
117
- const current = await readIfExists(target);
118
- if (current === null)
119
- continue;
120
- const line = homePointerLine(root.dir, home);
121
- const lines = current.split('\n');
122
- const kept = lines.filter((entry) => entry.trim() !== line);
123
- if (kept.length === lines.length)
124
- continue;
125
- const rebuilt = kept.join('\n').replace(/\n{3,}$/, '\n\n').replace(/\n+$/, '\n');
126
- if (rebuilt.trim() === '' || rebuilt.trim() === HOME_FILE_HEADING) {
127
- await fsp.unlink(target).catch(() => undefined);
128
- }
129
- else {
130
- await writeFileAtomic(target, rebuilt, { mode: 'preserve' });
131
- }
132
- results.push({ file: target, status: 'removed' });
133
- }
134
- return { results };
135
- }