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/hooks/_upgrade.mjs
DELETED
|
@@ -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));
|
package/hooks/session-report.mjs
DELETED
|
@@ -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));
|