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,51 +0,0 @@
1
- import { readFileSync, writeFileSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- const NOTICE_INTERVAL_MS = 24 * 60 * 60 * 1000;
4
- const RELEASE_BASENAME = 'cli-release.json';
5
- const STAMP_BASENAME = 'upgrade-notified-at';
6
- export function renderUpgradeNotice(release) {
7
- if (release === null || typeof release !== 'object')
8
- return null;
9
- if (release.behind !== true)
10
- return null;
11
- const latest = typeof release.latest === 'string' ? release.latest.trim() : '';
12
- const command = typeof release.command === 'string' ? release.command.trim() : '';
13
- if (latest === '' || command === '')
14
- return null;
15
- return `⚓ harbor ${latest} is available. Run: ${command}`;
16
- }
17
- function dueForNotice(stampPath, now) {
18
- let raw;
19
- try {
20
- raw = readFileSync(stampPath, 'utf8');
21
- }
22
- catch {
23
- return true;
24
- }
25
- const last = Date.parse(raw.trim());
26
- if (!Number.isFinite(last))
27
- return false;
28
- return now - last >= NOTICE_INTERVAL_MS;
29
- }
30
- export function upgradeNotice(harborDir, now = Date.now()) {
31
- let release;
32
- try {
33
- release = JSON.parse(readFileSync(join(harborDir, RELEASE_BASENAME), 'utf8'));
34
- }
35
- catch {
36
- return null;
37
- }
38
- const line = renderUpgradeNotice(release);
39
- if (line === null)
40
- return null;
41
- const stampPath = join(harborDir, STAMP_BASENAME);
42
- if (!dueForNotice(stampPath, now))
43
- return null;
44
- try {
45
- writeFileSync(stampPath, `${new Date(now).toISOString()}\n`, { mode: 0o600 });
46
- }
47
- catch {
48
- return null;
49
- }
50
- return line;
51
- }
@@ -1,60 +0,0 @@
1
- #!/usr/bin/env node
2
- import { CAPTURE_MAX_RETRIES, CAPTURE_TRIGGER, adoptHookInput, capturePassOutcome, gitOrigin, gitRoot, hasCaptureMarker, normalizeOriginUrl, pruneCaptureMarkers, readAuthHeader, readCaptureMarker, readStdin, readSyncedConfig, repoArms, sessionDidRepoWork, settleCaptureMarker, writeCaptureMarker, } from './_shared.mjs';
3
- function emitBlock(reason) {
4
- const payload = JSON.stringify({ decision: 'block', reason });
5
- return new Promise((resolve) => {
6
- process.stdout.write(payload, () => resolve());
7
- });
8
- }
9
- function captureRetryCount(sessionId, transcriptPath) {
10
- if (!hasCaptureMarker(sessionId))
11
- return 0;
12
- const marker = readCaptureMarker(sessionId);
13
- if (marker?.settled)
14
- return null;
15
- const retries = Number.isInteger(marker?.retries) ? marker.retries : 0;
16
- if (retries >= CAPTURE_MAX_RETRIES) {
17
- settleCaptureMarker(sessionId);
18
- return null;
19
- }
20
- if (capturePassOutcome(transcriptPath) !== 'rejected') {
21
- settleCaptureMarker(sessionId);
22
- return null;
23
- }
24
- return retries + 1;
25
- }
26
- async function main() {
27
- const raw = await readStdin();
28
- let input;
29
- try {
30
- input = JSON.parse(raw);
31
- }
32
- catch {
33
- return;
34
- }
35
- if (input?.stop_hook_active === true)
36
- return;
37
- const { sessionId, cwd, transcriptPath } = adoptHookInput(input);
38
- if (!sessionId || !cwd)
39
- return;
40
- const retryCount = captureRetryCount(sessionId, transcriptPath);
41
- if (retryCount === null)
42
- return;
43
- const config = readSyncedConfig();
44
- if (!config)
45
- return;
46
- const originUrl = normalizeOriginUrl(gitOrigin(cwd) ?? '');
47
- if (!originUrl || !config.origins.has(originUrl))
48
- return;
49
- if (!repoArms(config, originUrl).capture)
50
- return;
51
- if (!readAuthHeader())
52
- return;
53
- const root = gitRoot(cwd) ?? cwd;
54
- if (!sessionDidRepoWork(transcriptPath, root, cwd))
55
- return;
56
- writeCaptureMarker(sessionId, retryCount);
57
- pruneCaptureMarkers();
58
- await emitBlock(CAPTURE_TRIGGER);
59
- }
60
- main().then(() => process.exit(0), () => process.exit(0));
@@ -1,19 +0,0 @@
1
- #!/usr/bin/env node
2
- import { adoptHookInput, readStdin, updateMarkerRecall } from './_shared.mjs';
3
- async function main() {
4
- const raw = await readStdin();
5
- let input;
6
- try {
7
- input = JSON.parse(raw);
8
- }
9
- catch {
10
- return;
11
- }
12
- const { sessionId, event } = adoptHookInput(input);
13
- if (!sessionId)
14
- return;
15
- if (event === 'SessionStart' && input?.source !== 'compact')
16
- return;
17
- updateMarkerRecall(sessionId, { contextReset: true });
18
- }
19
- main().then(() => process.exit(0), () => process.exit(0));
@@ -1,258 +0,0 @@
1
- #!/usr/bin/env node
2
- import { CAPTURE_BAR, adoptHookInput, DRAIN_IDLE_MS, HARBOR_DIR, MAX_DRAIN, MAX_SCRAPE_PER_DRAIN, RECALL_OUTCOMES, gitOrigin, hookAgent, listMarkers, markReported, markerPathFor, normalizeOriginUrl, lastRequestFailureReason, postJson, readAuthHeader, readMarker, readStdin, readSyncedConfig, recordRecallOutcome, refreshSyncedOrigins, repoArms, scanForContextTruncation, scrapeSessionTranscript, SENTINEL_NAMES, setRecallOrigin, touchMarker, updateMarkerEmptyAsk, updateMarkerRecall, updateMarkerSignature, wrapRedelivery, writeMarker, } from './_shared.mjs';
3
- import { askedAndEmpty, buildRecallQuery, commitSignature, evaluateShift, foldEmptyAsk, } from './_recall.mjs';
4
- import { receiptCapable, renderReceipt } from './_receipt.mjs';
5
- import { upgradeNotice } from './_upgrade.mjs';
6
- let deliveredReceipt = null;
7
- const DESKTOP_CHAT = process.env.MILESTONE_DESKTOP_CHAT === '1';
8
- const DELTA_CHAR_BUDGET = 1200;
9
- const DELTA_LEAD = 'New team context surfaced for this turn:';
10
- async function drainOtherSessions(apiBase, authHeader, currentSid) {
11
- const now = Date.now();
12
- const stale = listMarkers()
13
- .filter((m) => !m.reported &&
14
- m.sessionId !== currentSid &&
15
- now - m.lastSeenAt >= DRAIN_IDLE_MS)
16
- .slice(0, MAX_DRAIN);
17
- if (stale.length === 0)
18
- return;
19
- const scrapeOrder = [...stale].sort((a, b) => (b.createdAt ?? 0) - (a.createdAt ?? 0));
20
- const scrapable = new Set(scrapeOrder.slice(0, MAX_SCRAPE_PER_DRAIN).map((m) => m.sessionId));
21
- await Promise.all(stale.map(async (m) => {
22
- const scrape = scrapable.has(m.sessionId)
23
- ? scrapeSessionTranscript(m.transcriptPath)
24
- : null;
25
- const res = await postJson(`${apiBase}/context/session-report`, authHeader, scrape === null
26
- ? { sessionId: m.sessionId }
27
- : {
28
- sessionId: m.sessionId,
29
- citedOrdinals: scrape.ordinals,
30
- scraperSentinels: SENTINEL_NAMES,
31
- ...(scrape.learningRefs.length > 0
32
- ? { learningRefs: scrape.learningRefs }
33
- : {}),
34
- });
35
- if (res && res.status === 200)
36
- markReported(m.sessionId, m.path);
37
- }));
38
- }
39
- function emitAdditionalContext(additionalContext, systemMessage = null) {
40
- const agent = hookAgent();
41
- const receipt = systemMessage && receiptCapable(agent) ? { systemMessage } : {};
42
- const context = typeof additionalContext === 'string' && additionalContext !== ''
43
- ? additionalContext
44
- : null;
45
- if (agent === 'cursor' && context === null)
46
- return Promise.resolve();
47
- const payload = agent === 'cursor'
48
- ? JSON.stringify({ additional_context: context })
49
- : JSON.stringify({
50
- ...(context === null
51
- ? {}
52
- : {
53
- hookSpecificOutput: {
54
- hookEventName: 'UserPromptSubmit',
55
- additionalContext: context,
56
- },
57
- }),
58
- ...receipt,
59
- });
60
- return new Promise((resolve) => {
61
- process.stdout.write(payload, () => resolve());
62
- });
63
- }
64
- function wrapContext(block) {
65
- return `<prior_knowledge>\n${block}\n</prior_knowledge>`;
66
- }
67
- function wrapDelta(block) {
68
- return `<prior_knowledge>\n${DELTA_LEAD}\n${block}\n</prior_knowledge>`;
69
- }
70
- function paymentPausedNotice(bundle) {
71
- const said = (typeof bundle.body?.error === 'string' && bundle.body.error) ||
72
- (typeof bundle.body?.message === 'string' && bundle.body.message) ||
73
- 'This workspace is past due.';
74
- return (`<prior_knowledge>\n` +
75
- `harbor team recall is paused for this workspace. ${said}\n` +
76
- `Tell the user this once, plainly, then carry on without team context.\n` +
77
- `</prior_knowledge>`);
78
- }
79
- async function firstPromptRead(apiBase, authHeader, ctx) {
80
- const outcomeFor = (ok, fallback) => ctx.redelivery
81
- ? ok
82
- ? RECALL_OUTCOMES.REDELIVERED_AFTER_COMPACT
83
- : RECALL_OUTCOMES.CONTEXT_LOST
84
- : fallback;
85
- const stage = ctx.redelivery ? 'redelivery' : 'first';
86
- const bundle = await postJson(`${apiBase}/context/session-bundle`, authHeader, {
87
- taskContext: ctx.prompt,
88
- sessionId: ctx.sessionId,
89
- originUrl: ctx.originUrl,
90
- cwd: ctx.cwd,
91
- ...(ctx.redelivery ? { restore: true } : {}),
92
- });
93
- if (!bundle) {
94
- recordRecallOutcome(ctx.sessionId, outcomeFor(false, RECALL_OUTCOMES.REQUEST_FAILED), `${stage}:${lastRequestFailureReason()}`);
95
- return null;
96
- }
97
- if (bundle.status === 402) {
98
- writeMarker(ctx.sessionId, false, ctx.query
99
- ? commitSignature(ctx.marker ? ctx.marker.signature : [], ctx.query.tokens)
100
- : undefined, ctx.transcriptPath);
101
- recordRecallOutcome(ctx.sessionId, RECALL_OUTCOMES.PAYMENT_REQUIRED, stage);
102
- return paymentPausedNotice(bundle);
103
- }
104
- if (bundle.status === 200 || bundle.status === 204) {
105
- writeMarker(ctx.sessionId, false, ctx.query
106
- ? commitSignature(ctx.marker ? ctx.marker.signature : [], ctx.query.tokens)
107
- : undefined, ctx.transcriptPath);
108
- if (bundle.status === 200 &&
109
- typeof bundle.body?.block === 'string' &&
110
- bundle.body.block.length > 0) {
111
- recordRecallOutcome(ctx.sessionId, outcomeFor(true, RECALL_OUTCOMES.DELIVERED));
112
- updateMarkerRecall(ctx.sessionId, { deliveredAt: new Date().toISOString() });
113
- deliveredReceipt = renderReceipt(bundle.body.receipt);
114
- return ctx.redelivery
115
- ? wrapRedelivery(bundle.body.block)
116
- : wrapContext(bundle.body.block);
117
- }
118
- recordRecallOutcome(ctx.sessionId, outcomeFor(false, RECALL_OUTCOMES.EMPTY_BUNDLE), `${stage}:${bundle.status}`);
119
- return null;
120
- }
121
- recordRecallOutcome(ctx.sessionId, outcomeFor(false, RECALL_OUTCOMES.REQUEST_FAILED), `${stage}:${bundle.status}`);
122
- return null;
123
- }
124
- async function maybeDelta(apiBase, authHeader, ctx) {
125
- if (!ctx.query) {
126
- recordRecallOutcome(ctx.sessionId, RECALL_OUTCOMES.NO_QUERY);
127
- return null;
128
- }
129
- const { shift, newTokens } = evaluateShift(ctx.marker.signature, ctx.query.tokens);
130
- if (!shift) {
131
- recordRecallOutcome(ctx.sessionId, RECALL_OUTCOMES.NO_SHIFT);
132
- return null;
133
- }
134
- if (askedAndEmpty(newTokens, ctx.marker.emptyAsk)) {
135
- recordRecallOutcome(ctx.sessionId, RECALL_OUTCOMES.NO_SHIFT, 'asked-empty');
136
- return null;
137
- }
138
- updateMarkerSignature(ctx.sessionId, commitSignature(ctx.marker.signature, ctx.query.tokens));
139
- const bundle = await postJson(`${apiBase}/context/session-bundle`, authHeader, {
140
- taskContext: ctx.query.query,
141
- sessionId: ctx.sessionId,
142
- originUrl: ctx.originUrl,
143
- cwd: ctx.cwd,
144
- charBudget: DELTA_CHAR_BUDGET,
145
- includeAlwaysInject: false,
146
- });
147
- if (!bundle) {
148
- recordRecallOutcome(ctx.sessionId, RECALL_OUTCOMES.REQUEST_FAILED, `delta:${lastRequestFailureReason()}`);
149
- return null;
150
- }
151
- if (bundle.status === 402) {
152
- recordRecallOutcome(ctx.sessionId, RECALL_OUTCOMES.PAYMENT_REQUIRED, 'delta');
153
- return null;
154
- }
155
- if (bundle.status === 200 || bundle.status === 204) {
156
- writeMarker(ctx.sessionId, false, undefined, ctx.transcriptPath);
157
- }
158
- if (bundle.status === 200 &&
159
- typeof bundle.body?.block === 'string' &&
160
- bundle.body.block.length > 0) {
161
- recordRecallOutcome(ctx.sessionId, RECALL_OUTCOMES.DELIVERED_DELTA);
162
- updateMarkerRecall(ctx.sessionId, { deliveredAt: new Date().toISOString() });
163
- deliveredReceipt = renderReceipt(bundle.body.receipt, { delta: true });
164
- return wrapDelta(bundle.body.block);
165
- }
166
- updateMarkerEmptyAsk(ctx.sessionId, foldEmptyAsk(ctx.marker.emptyAsk, ctx.query.tokens));
167
- recordRecallOutcome(ctx.sessionId, RECALL_OUTCOMES.EMPTY_BUNDLE, `delta:${bundle.status}`);
168
- return null;
169
- }
170
- async function main() {
171
- const raw = await readStdin();
172
- let input;
173
- try {
174
- input = JSON.parse(raw);
175
- }
176
- catch {
177
- return;
178
- }
179
- const { agent, sessionId, cwd, transcriptPath } = adoptHookInput(input);
180
- const prompt = typeof input?.prompt === 'string' ? input.prompt : '';
181
- if (!sessionId || !cwd)
182
- return;
183
- if (!prompt && agent !== 'cursor')
184
- return;
185
- const config = readSyncedConfig();
186
- if (!config) {
187
- recordRecallOutcome(sessionId, RECALL_OUTCOMES.NO_CONFIG);
188
- return;
189
- }
190
- const originUrl = normalizeOriginUrl(gitOrigin(cwd) ?? '');
191
- if (!originUrl) {
192
- recordRecallOutcome(sessionId, RECALL_OUTCOMES.NO_ORIGIN);
193
- return;
194
- }
195
- if (!config.origins.has(originUrl)) {
196
- const refreshed = await refreshSyncedOrigins();
197
- if (!refreshed || !refreshed.has(originUrl)) {
198
- recordRecallOutcome(sessionId, RECALL_OUTCOMES.NOT_ALLOWLISTED);
199
- return;
200
- }
201
- }
202
- setRecallOrigin(originUrl);
203
- const arms = repoArms(config, originUrl);
204
- if (!arms.recall) {
205
- recordRecallOutcome(sessionId, RECALL_OUTCOMES.RECALL_OFF);
206
- return;
207
- }
208
- const authHeader = readAuthHeader();
209
- if (!authHeader) {
210
- recordRecallOutcome(sessionId, RECALL_OUTCOMES.NO_AUTH);
211
- return;
212
- }
213
- const { apiBase } = config;
214
- if (DESKTOP_CHAT) {
215
- recordRecallOutcome(sessionId, RECALL_OUTCOMES.DESKTOP_CHAT);
216
- await drainOtherSessions(apiBase, authHeader, sessionId);
217
- return;
218
- }
219
- const marker = readMarker(markerPathFor(sessionId));
220
- if (marker && !marker.transcriptPath && transcriptPath) {
221
- writeMarker(sessionId, marker.reported, undefined, transcriptPath);
222
- }
223
- else if (marker) {
224
- touchMarker(sessionId);
225
- }
226
- const scan = scanForContextTruncation(transcriptPath, marker?.scanOffset);
227
- const contextLost = Boolean(marker &&
228
- (scan.truncated || marker.contextReset === true) &&
229
- typeof marker.deliveredAt === 'string');
230
- const query = buildRecallQuery({ text: prompt });
231
- const ctx = {
232
- sessionId,
233
- originUrl,
234
- cwd,
235
- prompt,
236
- query,
237
- marker,
238
- transcriptPath,
239
- redelivery: contextLost,
240
- };
241
- const [, knowledge] = await Promise.all([
242
- drainOtherSessions(apiBase, authHeader, sessionId),
243
- marker && !contextLost
244
- ? maybeDelta(apiBase, authHeader, ctx)
245
- : firstPromptRead(apiBase, authHeader, ctx),
246
- ]);
247
- updateMarkerRecall(sessionId, { scanOffset: scan.offset, contextReset: false });
248
- const parts = [knowledge, marker || !arms.capture ? null : CAPTURE_BAR].filter((part) => typeof part === 'string' && part.length > 0);
249
- const systemMessage = joinSystemMessages(config.receipt ? deliveredReceipt : null, upgradeNotice(HARBOR_DIR));
250
- if (parts.length > 0 || systemMessage !== null) {
251
- await emitAdditionalContext(parts.length > 0 ? parts.join('\n\n') : null, systemMessage);
252
- }
253
- }
254
- function joinSystemMessages(...messages) {
255
- const present = messages.filter((m) => typeof m === 'string' && m.trim() !== '');
256
- return present.length === 0 ? null : present.join('\n\n');
257
- }
258
- main().then(() => process.exit(0), () => process.exit(0));
@@ -1,42 +0,0 @@
1
- #!/usr/bin/env node
2
- import { adoptHookInput, markReported, markerPathFor, postJson, readAuthHeader, readMarker, readStdin, readSyncedConfig, scrapeSessionTranscript, SENTINEL_NAMES, } from './_shared.mjs';
3
- async function main() {
4
- const raw = await readStdin();
5
- let input;
6
- try {
7
- input = JSON.parse(raw);
8
- }
9
- catch {
10
- return;
11
- }
12
- const { sessionId, transcriptPath, event } = adoptHookInput(input);
13
- if (!sessionId)
14
- return;
15
- const turnEnd = event === 'Stop';
16
- const marker = readMarker(markerPathFor(sessionId));
17
- if (!marker || marker.reported === true)
18
- return;
19
- const config = readSyncedConfig();
20
- if (!config)
21
- return;
22
- const authHeader = readAuthHeader();
23
- if (!authHeader)
24
- return;
25
- const scrape = turnEnd ? null : scrapeSessionTranscript(transcriptPath);
26
- const res = await postJson(`${config.apiBase}/context/session-report`, authHeader, scrape === null
27
- ? {
28
- sessionId,
29
- ...(turnEnd ? { interim: true } : {}),
30
- }
31
- : {
32
- sessionId,
33
- citedOrdinals: scrape.ordinals,
34
- scraperSentinels: SENTINEL_NAMES,
35
- ...(scrape.learningRefs.length > 0
36
- ? { learningRefs: scrape.learningRefs }
37
- : {}),
38
- });
39
- if (res && res.status === 200 && !turnEnd)
40
- markReported(sessionId);
41
- }
42
- main().then(() => process.exit(0), () => process.exit(0));
@@ -1,183 +0,0 @@
1
- #!/usr/bin/env node
2
- import { MIN_CHANGE_OVERLAP, basenameTokens, changeTokens, pathTokens, } from './_recall.mjs';
3
- import { RECALL_OUTCOMES, adoptHookInput, claimFiredKey, gitOrigin, hookAgent, markerPathFor, normalizeOriginUrl, noteToolLookup, postJson, readAuthHeader, readMarker, readStdin, readSyncedConfig, recordRecallOutcome, repoArms, setRecallOrigin, toolLookupsSuspended, updateMarkerRecall, wrapRedelivery, } from './_shared.mjs';
4
- const TOOL_CHAR_BUDGET = 700;
5
- const TOOL_REQUEST_TIMEOUT_MS = 1200;
6
- const RESTORE_REQUEST_TIMEOUT_MS = 3000;
7
- const CHANGE_QUERY_CHARS = 800;
8
- const LEAD = 'Team knowledge about what you are touching (from your team graph, not the file):';
9
- const RISKY_COMMAND = [
10
- /\bprisma\s+(migrate|db\s+push)/,
11
- /\bgit\s+(checkout|stash|reset)\b/,
12
- /\bgit\s+push\b/,
13
- /\brm\s+-[a-z]*r/,
14
- /\b(docker\s+(build|push)|terraform\s+(apply|destroy))\b/,
15
- ];
16
- function emitContext(text) {
17
- const payload = hookAgent() === 'cursor'
18
- ? JSON.stringify({ additional_context: text })
19
- : JSON.stringify({
20
- hookSpecificOutput: {
21
- hookEventName: 'PostToolUse',
22
- additionalContext: text,
23
- },
24
- });
25
- return new Promise((resolve) => {
26
- process.stdout.write(payload, () => resolve());
27
- });
28
- }
29
- function emitDecision(decision, reason) {
30
- const payload = hookAgent() === 'cursor'
31
- ? JSON.stringify({ permission: decision, user_message: reason })
32
- : JSON.stringify({
33
- hookSpecificOutput: {
34
- hookEventName: 'PreToolUse',
35
- permissionDecision: decision,
36
- permissionDecisionReason: reason,
37
- },
38
- });
39
- return new Promise((resolve) => {
40
- process.stdout.write(payload, () => resolve());
41
- });
42
- }
43
- function namesTheFile(block, filePath) {
44
- const tokens = basenameTokens(filePath);
45
- if (tokens.length === 0)
46
- return false;
47
- const haystack = block.toLowerCase();
48
- return tokens.some((token) => haystack.includes(token));
49
- }
50
- function worthInjecting(block, filePath, change) {
51
- const changeWords = changeTokens(change);
52
- if (changeWords.size < MIN_CHANGE_OVERLAP)
53
- return namesTheFile(block, filePath);
54
- let overlap = 0;
55
- for (const word of changeTokens(block)) {
56
- if (changeWords.has(word) && ++overlap >= MIN_CHANGE_OVERLAP)
57
- return true;
58
- }
59
- return false;
60
- }
61
- async function restoreLostContext(config, ctx, taskContext) {
62
- const marker = readMarker(markerPathFor(ctx.sessionId));
63
- if (!marker || marker.contextReset !== true)
64
- return null;
65
- updateMarkerRecall(ctx.sessionId, { contextReset: false });
66
- if (typeof marker.deliveredAt !== 'string')
67
- return null;
68
- const authHeader = readAuthHeader();
69
- if (!authHeader) {
70
- recordRecallOutcome(ctx.sessionId, RECALL_OUTCOMES.CONTEXT_LOST, 'cursor-tool:no-auth');
71
- return null;
72
- }
73
- const bundle = await postJson(`${config.apiBase}/context/session-bundle`, authHeader, {
74
- restore: true,
75
- taskContext,
76
- sessionId: ctx.sessionId,
77
- originUrl: ctx.originUrl,
78
- cwd: ctx.cwd,
79
- }, RESTORE_REQUEST_TIMEOUT_MS);
80
- if (!bundle || bundle.status !== 200) {
81
- recordRecallOutcome(ctx.sessionId, RECALL_OUTCOMES.CONTEXT_LOST, `cursor-tool:${bundle ? bundle.status : 'network'}`);
82
- return null;
83
- }
84
- const text = bundle.body?.block;
85
- if (typeof text !== 'string' || text.length === 0) {
86
- recordRecallOutcome(ctx.sessionId, RECALL_OUTCOMES.CONTEXT_LOST, 'cursor-tool:empty');
87
- return null;
88
- }
89
- recordRecallOutcome(ctx.sessionId, RECALL_OUTCOMES.REDELIVERED_AFTER_COMPACT, 'cursor-tool');
90
- updateMarkerRecall(ctx.sessionId, { deliveredAt: new Date().toISOString() });
91
- return wrapRedelivery(text);
92
- }
93
- async function fetchBlock(apiBase, authHeader, ctx, taskContext) {
94
- const bundle = await postJson(`${apiBase}/context/session-bundle`, authHeader, {
95
- taskContext,
96
- sessionId: ctx.sessionId,
97
- originUrl: ctx.originUrl,
98
- cwd: ctx.cwd,
99
- charBudget: TOOL_CHAR_BUDGET,
100
- includeAlwaysInject: false,
101
- toolContext: true,
102
- }, TOOL_REQUEST_TIMEOUT_MS);
103
- if (!bundle || bundle.status !== 200)
104
- return null;
105
- const block = bundle.body?.block;
106
- return typeof block === 'string' && block.length > 0 ? block : null;
107
- }
108
- async function main() {
109
- const raw = await readStdin();
110
- let input;
111
- try {
112
- input = JSON.parse(raw);
113
- }
114
- catch {
115
- return;
116
- }
117
- const { sessionId, cwd, event, toolName } = adoptHookInput(input);
118
- const toolInput = input?.tool_input ?? {};
119
- if (!sessionId || !cwd || !event)
120
- return;
121
- const config = readSyncedConfig();
122
- if (!config)
123
- return;
124
- const originUrl = normalizeOriginUrl(gitOrigin(cwd) ?? '');
125
- if (!originUrl || !config.origins.has(originUrl))
126
- return;
127
- setRecallOrigin(originUrl);
128
- if (!repoArms(config, originUrl).recall)
129
- return;
130
- const ctx = { sessionId, originUrl, cwd };
131
- if (event === 'PostToolUse' && (toolName === 'Write' || toolName === 'Edit')) {
132
- const filePath = typeof toolInput.file_path === 'string' ? toolInput.file_path : '';
133
- if (!filePath)
134
- return;
135
- const change = typeof toolInput.new_string === 'string'
136
- ? toolInput.new_string
137
- : typeof toolInput.content === 'string'
138
- ? toolInput.content
139
- : '';
140
- const editQuery = `Editing ${filePath} (${pathTokens(filePath).join(' ')})\n${change.slice(0, CHANGE_QUERY_CHARS)}`;
141
- if (hookAgent() === 'cursor') {
142
- const restored = await restoreLostContext(config, ctx, editQuery);
143
- if (restored) {
144
- await emitContext(restored);
145
- return;
146
- }
147
- }
148
- if (basenameTokens(filePath).length === 0 && changeTokens(change).size === 0) {
149
- return;
150
- }
151
- if (toolLookupsSuspended(sessionId))
152
- return;
153
- if (!claimFiredKey(sessionId, `path:${filePath}`))
154
- return;
155
- const authHeader = readAuthHeader();
156
- if (!authHeader)
157
- return;
158
- const block = await fetchBlock(config.apiBase, authHeader, ctx, editQuery);
159
- const useful = Boolean(block) && worthInjecting(block, filePath, change);
160
- noteToolLookup(sessionId, Boolean(block));
161
- if (useful)
162
- await emitContext(`${LEAD}\n${block}`);
163
- return;
164
- }
165
- if (event === 'PreToolUse' && toolName === 'Bash') {
166
- if (config.toolGuard !== true)
167
- return;
168
- const command = typeof toolInput.command === 'string' ? toolInput.command : '';
169
- if (!command || !RISKY_COMMAND.some((re) => re.test(command)))
170
- return;
171
- if (!claimFiredKey(sessionId, `cmd:${command.slice(0, 120)}`))
172
- return;
173
- const authHeader = readAuthHeader();
174
- if (!authHeader)
175
- return;
176
- const block = await fetchBlock(config.apiBase, authHeader, ctx, `About to run: ${command.slice(0, 300)}`);
177
- if (!block)
178
- return;
179
- await emitDecision('deny', `Your team's graph has a rule covering this command. Read it, then re-run ` +
180
- `if it still applies:\n${block}`);
181
- }
182
- }
183
- main().then(() => process.exit(0), () => process.exit(0));