harborloop 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/bin/checksums.json +12 -0
  2. package/bin/harbor.mjs +188 -0
  3. package/package.json +14 -30
  4. package/dist/api.d.ts +0 -109
  5. package/dist/api.js +0 -409
  6. package/dist/auth/login.d.ts +0 -14
  7. package/dist/auth/login.js +0 -158
  8. package/dist/auth/loopback.d.ts +0 -11
  9. package/dist/auth/loopback.js +0 -87
  10. package/dist/auth/pkce.d.ts +0 -7
  11. package/dist/auth/pkce.js +0 -14
  12. package/dist/auth/store.d.ts +0 -26
  13. package/dist/auth/store.js +0 -107
  14. package/dist/cli-release.d.ts +0 -31
  15. package/dist/cli-release.js +0 -102
  16. package/dist/commands/bind.d.ts +0 -23
  17. package/dist/commands/bind.js +0 -86
  18. package/dist/commands/context.d.ts +0 -12
  19. package/dist/commands/context.js +0 -80
  20. package/dist/commands/daemon.d.ts +0 -39
  21. package/dist/commands/daemon.js +0 -140
  22. package/dist/commands/doctor.d.ts +0 -16
  23. package/dist/commands/doctor.js +0 -937
  24. package/dist/commands/init.d.ts +0 -36
  25. package/dist/commands/init.js +0 -192
  26. package/dist/commands/status.d.ts +0 -5
  27. package/dist/commands/status.js +0 -164
  28. package/dist/commands/sync.d.ts +0 -40
  29. package/dist/commands/sync.js +0 -242
  30. package/dist/commands/toggle.d.ts +0 -16
  31. package/dist/commands/toggle.js +0 -69
  32. package/dist/commands/unbind.d.ts +0 -14
  33. package/dist/commands/unbind.js +0 -60
  34. package/dist/commands/workspaces.d.ts +0 -18
  35. package/dist/commands/workspaces.js +0 -41
  36. package/dist/context-budget.d.ts +0 -40
  37. package/dist/context-budget.js +0 -259
  38. package/dist/daemon/install.d.ts +0 -50
  39. package/dist/daemon/install.js +0 -192
  40. package/dist/daemon/loop.d.ts +0 -15
  41. package/dist/daemon/loop.js +0 -84
  42. package/dist/daemon/state.d.ts +0 -79
  43. package/dist/daemon/state.js +0 -213
  44. package/dist/daemon/tick.d.ts +0 -37
  45. package/dist/daemon/tick.js +0 -231
  46. package/dist/documents/apply.d.ts +0 -21
  47. package/dist/documents/apply.js +0 -58
  48. package/dist/documents/bundle.d.ts +0 -41
  49. package/dist/documents/bundle.js +0 -43
  50. package/dist/documents/inspect.d.ts +0 -12
  51. package/dist/documents/inspect.js +0 -33
  52. package/dist/documents/manifest.d.ts +0 -14
  53. package/dist/documents/manifest.js +0 -52
  54. package/dist/documents/pointer.d.ts +0 -32
  55. package/dist/documents/pointer.js +0 -135
  56. package/dist/documents/write.d.ts +0 -57
  57. package/dist/documents/write.js +0 -211
  58. package/dist/endpoints.d.ts +0 -7
  59. package/dist/endpoints.js +0 -15
  60. package/dist/env-bundle.d.ts +0 -25
  61. package/dist/env-bundle.js +0 -33
  62. package/dist/env-scan.d.ts +0 -27
  63. package/dist/env-scan.js +0 -41
  64. package/dist/env-sync.d.ts +0 -31
  65. package/dist/env-sync.js +0 -37
  66. package/dist/index.d.ts +0 -11
  67. package/dist/index.js +0 -282
  68. package/dist/install-hooks.d.ts +0 -11
  69. package/dist/install-hooks.js +0 -64
  70. package/dist/lock.d.ts +0 -31
  71. package/dist/lock.js +0 -192
  72. package/dist/machine.d.ts +0 -13
  73. package/dist/machine.js +0 -53
  74. package/dist/memories.d.ts +0 -31
  75. package/dist/memories.js +0 -74
  76. package/dist/origin.d.ts +0 -4
  77. package/dist/origin.js +0 -59
  78. package/dist/paths.d.ts +0 -12
  79. package/dist/paths.js +0 -45
  80. package/dist/providers/claude.d.ts +0 -65
  81. package/dist/providers/claude.js +0 -412
  82. package/dist/providers/codex-trust.d.ts +0 -36
  83. package/dist/providers/codex-trust.js +0 -141
  84. package/dist/providers/codex.d.ts +0 -46
  85. package/dist/providers/codex.js +0 -520
  86. package/dist/providers/cursor.d.ts +0 -45
  87. package/dist/providers/cursor.js +0 -344
  88. package/dist/providers/detect.d.ts +0 -16
  89. package/dist/providers/detect.js +0 -70
  90. package/dist/providers/hook-coverage.d.ts +0 -5
  91. package/dist/providers/hook-coverage.js +0 -47
  92. package/dist/providers/hook-entry.d.ts +0 -1
  93. package/dist/providers/hook-entry.js +0 -8
  94. package/dist/providers/surfaces.d.ts +0 -19
  95. package/dist/providers/surfaces.js +0 -91
  96. package/dist/reclaim.d.ts +0 -17
  97. package/dist/reclaim.js +0 -33
  98. package/dist/render.d.ts +0 -17
  99. package/dist/render.js +0 -61
  100. package/dist/repo-sync.d.ts +0 -27
  101. package/dist/repo-sync.js +0 -34
  102. package/dist/repos.d.ts +0 -11
  103. package/dist/repos.js +0 -82
  104. package/dist/skills.d.ts +0 -84
  105. package/dist/skills.js +0 -383
  106. package/dist/slugify.d.ts +0 -1
  107. package/dist/slugify.js +0 -8
  108. package/dist/state.d.ts +0 -23
  109. package/dist/state.js +0 -161
  110. package/dist/sync-report.d.ts +0 -59
  111. package/dist/sync-report.js +0 -87
  112. package/hooks/_recall.mjs +0 -288
  113. package/hooks/_receipt.mjs +0 -47
  114. package/hooks/_shared.mjs +0 -1610
  115. package/hooks/_upgrade.mjs +0 -51
  116. package/hooks/session-capture.mjs +0 -60
  117. package/hooks/session-compact.mjs +0 -19
  118. package/hooks/session-context.mjs +0 -258
  119. package/hooks/session-report.mjs +0 -42
  120. package/hooks/session-toolcontext.mjs +0 -183
package/dist/slugify.js DELETED
@@ -1,8 +0,0 @@
1
- export function slugify(name) {
2
- const lowered = name.toLowerCase();
3
- const mapped = Array.from(lowered, (ch) => (/[a-z0-9-]/.test(ch) ? ch : '-')).join('');
4
- return mapped
5
- .split('-')
6
- .filter((part) => part.length > 0)
7
- .join('-');
8
- }
package/dist/state.d.ts DELETED
@@ -1,23 +0,0 @@
1
- export type PauseMode = 'off' | 'capture' | 'on';
2
- export interface PauseState {
3
- readonly global: PauseMode | null;
4
- readonly repos: Readonly<Record<string, PauseMode>>;
5
- }
6
- export interface SyncedConfig {
7
- readonly apiBase: string;
8
- readonly origins: readonly string[];
9
- readonly pause: PauseState;
10
- readonly receipt: boolean;
11
- }
12
- export declare const EMPTY_PAUSE: PauseState;
13
- export interface RepoArms {
14
- readonly recall: boolean;
15
- readonly capture: boolean;
16
- }
17
- export declare function resolveArms(pause: PauseState, originUrl: string): RepoArms;
18
- export declare function readSyncedConfig(): Promise<SyncedConfig | null>;
19
- export declare function updateSyncedConfig(mutate: (current: SyncedConfig) => SyncedConfig): Promise<SyncedConfig>;
20
- export declare function withRepoPause(config: SyncedConfig, originUrl: string, mode: PauseMode | null): SyncedConfig;
21
- export declare function withReceipt(config: SyncedConfig, receipt: boolean): SyncedConfig;
22
- export declare function withGlobalPause(config: SyncedConfig, mode: PauseMode | null): SyncedConfig;
23
- export declare function migrateLegacyState(): Promise<'migrated' | 'skipped'>;
package/dist/state.js DELETED
@@ -1,161 +0,0 @@
1
- import { mkdir, readFile } from 'node:fs/promises';
2
- import { updateFileLocked } from './lock.js';
3
- import { normalizeOriginUrl } from './origin.js';
4
- import { harborDir, legacyDirs, syncedOriginsFile } from './paths.js';
5
- export const EMPTY_PAUSE = { global: null, repos: {} };
6
- export function resolveArms(pause, originUrl) {
7
- const mode = pause.repos[originUrl] ?? pause.global;
8
- if (mode === 'off')
9
- return { recall: false, capture: false };
10
- if (mode === 'capture')
11
- return { recall: true, capture: false };
12
- return { recall: true, capture: true };
13
- }
14
- const isMode = (value) => value === 'off' || value === 'capture' || value === 'on';
15
- function parsePause(raw) {
16
- if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
17
- return EMPTY_PAUSE;
18
- }
19
- const obj = raw;
20
- const global = isMode(obj.global) ? obj.global : null;
21
- const repos = {};
22
- const rawRepos = obj.repos;
23
- if (rawRepos !== null && typeof rawRepos === 'object' && !Array.isArray(rawRepos)) {
24
- for (const [key, value] of Object.entries(rawRepos)) {
25
- if (isMode(value))
26
- repos[normalizeOriginUrl(key)] = value;
27
- }
28
- }
29
- return { global, repos };
30
- }
31
- export async function readSyncedConfig() {
32
- let raw;
33
- try {
34
- raw = await readFile(syncedOriginsFile(), 'utf8');
35
- }
36
- catch {
37
- return null;
38
- }
39
- let parsed;
40
- try {
41
- parsed = JSON.parse(raw);
42
- }
43
- catch {
44
- return null;
45
- }
46
- if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
47
- return null;
48
- }
49
- const obj = parsed;
50
- if (typeof obj.apiBase !== 'string' || !Array.isArray(obj.origins))
51
- return null;
52
- const legacyCaptureOff = obj.capture === false;
53
- const pause = parsePause(obj.pause);
54
- return {
55
- apiBase: obj.apiBase.replace(/\/+$/, ''),
56
- origins: obj.origins
57
- .map((o) => normalizeOriginUrl(String(o)))
58
- .filter((o) => o !== ''),
59
- pause: legacyCaptureOff && pause.global === null
60
- ? { ...pause, global: 'capture' }
61
- : pause,
62
- receipt: readReceipt(obj),
63
- };
64
- }
65
- function serialise(config) {
66
- const body = {
67
- apiBase: config.apiBase,
68
- origins: [...config.origins],
69
- };
70
- if (config.pause.global !== null || Object.keys(config.pause.repos).length > 0) {
71
- body.pause = { global: config.pause.global, repos: config.pause.repos };
72
- }
73
- if (!config.receipt)
74
- body.receipt = false;
75
- return `${JSON.stringify(body, null, 2)}\n`;
76
- }
77
- export async function updateSyncedConfig(mutate) {
78
- await mkdir(harborDir(), { recursive: true, mode: 0o700 });
79
- let result;
80
- await updateFileLocked(syncedOriginsFile(), (existing) => {
81
- const current = parseOrEmpty(existing);
82
- result = mutate(current);
83
- return serialise(result);
84
- });
85
- return result;
86
- }
87
- function parseOrEmpty(existing) {
88
- if (existing.trim() === '') {
89
- return { apiBase: '', origins: [], pause: EMPTY_PAUSE, receipt: true };
90
- }
91
- try {
92
- const obj = JSON.parse(existing);
93
- return {
94
- apiBase: typeof obj.apiBase === 'string' ? obj.apiBase.replace(/\/+$/, '') : '',
95
- origins: Array.isArray(obj.origins)
96
- ? obj.origins.map((o) => normalizeOriginUrl(String(o))).filter((o) => o !== '')
97
- : [],
98
- pause: obj.capture === false && parsePause(obj.pause).global === null
99
- ? { ...parsePause(obj.pause), global: 'capture' }
100
- : parsePause(obj.pause),
101
- receipt: readReceipt(obj),
102
- };
103
- }
104
- catch {
105
- return { apiBase: '', origins: [], pause: EMPTY_PAUSE, receipt: true };
106
- }
107
- }
108
- function readReceipt(obj) {
109
- return obj.receipt !== false;
110
- }
111
- export function withRepoPause(config, originUrl, mode) {
112
- const repos = { ...config.pause.repos };
113
- if (mode === null)
114
- delete repos[originUrl];
115
- else
116
- repos[originUrl] = mode;
117
- return { ...config, pause: { ...config.pause, repos } };
118
- }
119
- export function withReceipt(config, receipt) {
120
- return { ...config, receipt };
121
- }
122
- export function withGlobalPause(config, mode) {
123
- return { ...config, pause: { ...config.pause, global: mode } };
124
- }
125
- export async function migrateLegacyState() {
126
- const existing = await readSyncedConfig();
127
- if (existing)
128
- return 'skipped';
129
- let legacyRaw;
130
- for (const dir of legacyDirs()) {
131
- try {
132
- legacyRaw = await readFile(`${dir}/synced-origins.json`, 'utf8');
133
- break;
134
- }
135
- catch {
136
- }
137
- }
138
- if (legacyRaw === undefined)
139
- return 'skipped';
140
- let parsed;
141
- try {
142
- parsed = JSON.parse(legacyRaw);
143
- }
144
- catch {
145
- return 'skipped';
146
- }
147
- if (parsed === null || typeof parsed !== 'object')
148
- return 'skipped';
149
- const obj = parsed;
150
- if (typeof obj.apiBase !== 'string' || !Array.isArray(obj.origins))
151
- return 'skipped';
152
- await updateSyncedConfig(() => ({
153
- apiBase: obj.apiBase,
154
- origins: obj.origins
155
- .map((o) => normalizeOriginUrl(String(o)))
156
- .filter((o) => o !== ''),
157
- pause: obj.capture === false ? { global: 'capture', repos: {} } : EMPTY_PAUSE,
158
- receipt: readReceipt(obj),
159
- }));
160
- return 'migrated';
161
- }
@@ -1,59 +0,0 @@
1
- import { type Owner } from './context-budget.js';
2
- import { type EnvScanOptions } from './env-scan.js';
3
- export interface SyncReportFile {
4
- readonly kind: 'SKILL' | 'DOCUMENT';
5
- readonly agent: string;
6
- readonly dir: string;
7
- readonly file: string;
8
- readonly sha256: string | null;
9
- readonly state: 'ours' | 'forked' | 'missing' | 'unverifiable';
10
- }
11
- export interface SyncReportSkill {
12
- readonly name: string;
13
- readonly owner: Owner;
14
- readonly tokens: number;
15
- readonly undiscoverable: boolean;
16
- }
17
- export interface SyncReportBudget {
18
- readonly tokens: number;
19
- readonly unmeasuredLines: number;
20
- readonly skillsIndexed: number;
21
- readonly skillsManaged: number;
22
- readonly skillsOtherTool: number;
23
- readonly skillsUnowned: number;
24
- readonly skillsUndiscoverable: number;
25
- readonly skills: readonly SyncReportSkill[];
26
- }
27
- export interface SyncReport {
28
- readonly machineId: string;
29
- readonly hostname: string;
30
- readonly os: string;
31
- readonly osVersion: string;
32
- readonly cliVersion: string;
33
- readonly bundleHash?: string;
34
- readonly files: readonly SyncReportFile[];
35
- readonly budget?: SyncReportBudget;
36
- }
37
- export type PostSyncReportResult = {
38
- readonly kind: 'ok';
39
- readonly machines: number;
40
- } | {
41
- readonly kind: 'unsupported';
42
- } | {
43
- readonly kind: 'error';
44
- readonly status: number | 'network';
45
- readonly message: string;
46
- };
47
- export interface PostSyncReportInput {
48
- readonly apiBase: string;
49
- readonly workspaceId: string;
50
- readonly bearerToken: string;
51
- readonly report: SyncReport;
52
- readonly fetchImpl?: typeof fetch;
53
- }
54
- export declare function syncReportUrl(apiBase: string, workspaceId: string): string;
55
- export declare function measureReportBudget(options: EnvScanOptions): Promise<SyncReportBudget | null>;
56
- export declare function buildSyncReport(options?: EnvScanOptions & {
57
- readonly bundleHash?: string;
58
- }): Promise<SyncReport>;
59
- export declare function postSyncReport(input: PostSyncReportInput): Promise<PostSyncReportResult>;
@@ -1,87 +0,0 @@
1
- import { ApiError, apiRequest } from './api.js';
2
- import { measureContext } from './context-budget.js';
3
- import { scanEnv, scannedFiles } from './env-scan.js';
4
- import { readMachineFacts } from './machine.js';
5
- function stripTrailingSlash(s) {
6
- return s.endsWith('/') ? s.slice(0, -1) : s;
7
- }
8
- export function syncReportUrl(apiBase, workspaceId) {
9
- return (`${stripTrailingSlash(apiBase)}/workspaces/${encodeURIComponent(workspaceId)}` +
10
- `/context/sync-report`);
11
- }
12
- export async function measureReportBudget(options) {
13
- try {
14
- const budget = await measureContext({
15
- ...(options.home === undefined ? {} : { home: options.home }),
16
- ...(options.agents === undefined ? {} : { agents: options.agents }),
17
- repoPath: null,
18
- });
19
- return {
20
- tokens: budget.measuredTokens,
21
- unmeasuredLines: budget.unmeasuredLines,
22
- skillsIndexed: budget.skills.length - budget.undiscoverable,
23
- skillsManaged: budget.ownership.harbor,
24
- skillsOtherTool: budget.ownership['other-tool'],
25
- skillsUnowned: budget.ownership.nobody,
26
- skillsUndiscoverable: budget.undiscoverable,
27
- skills: budget.skills.map((skill) => ({
28
- name: skill.name,
29
- owner: skill.owner,
30
- tokens: skill.tokens,
31
- undiscoverable: skill.undiscoverable,
32
- })),
33
- };
34
- }
35
- catch {
36
- return null;
37
- }
38
- }
39
- export async function buildSyncReport(options = {}) {
40
- const { bundleHash, ...scanOptions } = options;
41
- const [facts, scan, budget] = await Promise.all([
42
- readMachineFacts(),
43
- scanEnv(scanOptions),
44
- measureReportBudget(scanOptions),
45
- ]);
46
- return {
47
- machineId: facts.id,
48
- hostname: facts.hostname,
49
- os: facts.os,
50
- osVersion: facts.osVersion,
51
- cliVersion: facts.cliVersion,
52
- ...(bundleHash === undefined ? {} : { bundleHash }),
53
- files: scannedFiles(scan).map((f) => ({
54
- kind: f.kind,
55
- agent: f.agent,
56
- dir: f.dir,
57
- file: f.file,
58
- sha256: f.sha256,
59
- state: f.state,
60
- })),
61
- ...(budget === null ? {} : { budget }),
62
- };
63
- }
64
- export async function postSyncReport(input) {
65
- const url = syncReportUrl(input.apiBase, input.workspaceId);
66
- try {
67
- const body = await apiRequest(url, {
68
- method: 'POST',
69
- bearer: input.bearerToken,
70
- body: input.report,
71
- ...(input.fetchImpl ? { fetchImpl: input.fetchImpl } : {}),
72
- });
73
- return { kind: 'ok', machines: body?.data?.machines ?? 1 };
74
- }
75
- catch (err) {
76
- if (!(err instanceof ApiError)) {
77
- return {
78
- kind: 'error',
79
- status: 'network',
80
- message: err instanceof Error ? err.message : String(err),
81
- };
82
- }
83
- if (err.status === 404)
84
- return { kind: 'unsupported' };
85
- return { kind: 'error', status: err.status ?? 'network', message: err.message };
86
- }
87
- }
package/hooks/_recall.mjs DELETED
@@ -1,288 +0,0 @@
1
- const TASK_KEY_RE = /\b[A-Z][A-Z0-9]+-\d+\b/g;
2
- const STOPWORDS = new Set([
3
- 'the', 'a', 'an', 'and', 'or', 'but', 'if', 'then', 'else', 'so', 'as',
4
- 'of', 'to', 'in', 'on', 'at', 'by', 'for', 'with', 'from', 'into', 'onto',
5
- 'about', 'over', 'under', 'out', 'up', 'off', 'this', 'that', 'these',
6
- 'those', 'it', 'its', 'here', 'there', 'all', 'any', 'some', 'no', 'not',
7
- 'i', 'im', 'me', 'my', 'mine', 'we', 'us', 'our', 'ours', 'you', 'your',
8
- 'yours', 'he', 'him', 'his', 'she', 'her', 'they', 'them', 'their',
9
- 'hi', 'hey', 'hello', 'yo', 'thanks', 'thank', 'thankyou', 'please', 'pls',
10
- 'ok', 'okay', 'kk', 'yeah', 'yep', 'yes', 'nope', 'sure', 'cool', 'nice',
11
- 'great', 'lol', 'hmm', 'oh', 'ah', 'well', 'btw', 'fyi', 'actually',
12
- 'what', 'whats', 'why', 'how', 'when', 'where', 'who', 'which', 'whom',
13
- 'can', 'could', 'would', 'should', 'shall', 'will', 'wont', 'cant', 'do',
14
- 'does', 'did', 'is', 'are', 'am', 'was', 'were', 'be', 'been', 'being',
15
- 'have', 'has', 'had', 'having', 'may', 'might', 'must',
16
- 'get', 'got', 'make', 'made', 'let', 'lets', 'just', 'like', 'want',
17
- 'need', 'try', 'use', 'using', 'go', 'going', 'now', 'also', 'really',
18
- 'very', 'much', 'more', 'most', 'than', 'too', 'still', 'again', 'back',
19
- 'think', 'know', 'see', 'look', 'looks', 'tell', 'show', 'give', 'take',
20
- 'thing', 'things', 'stuff', 'something', 'anything', 'everything', 'one',
21
- 'ones', 'way', 'ways', 'bit', 'lot', 'good', 'bad', 'right', 'wrong',
22
- ]);
23
- const ATTACHMENT_EXT_RE = /\.(?:png|jpe?g|gif|webp|svg|bmp|heic|heif|avif|pdf)$/i;
24
- export function tokenize(text) {
25
- return text
26
- .toLowerCase()
27
- .split(/[^a-z0-9]+/)
28
- .filter((t) => t.length >= 2);
29
- }
30
- export function distinctiveTokens(text) {
31
- const seen = new Set();
32
- const out = [];
33
- for (const tok of tokenize(text)) {
34
- if (STOPWORDS.has(tok) || seen.has(tok))
35
- continue;
36
- seen.add(tok);
37
- out.push(tok);
38
- }
39
- return out;
40
- }
41
- function isWhitespace(ch) {
42
- return ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r';
43
- }
44
- export function stripAttachmentMentions(text) {
45
- const pieces = [];
46
- let cursor = 0;
47
- let hadAttachment = false;
48
- while (cursor < text.length) {
49
- const at = text.indexOf('@', cursor);
50
- if (at === -1) {
51
- pieces.push(text.slice(cursor));
52
- break;
53
- }
54
- if (at > 0 && !isWhitespace(text[at - 1] ?? '')) {
55
- pieces.push(text.slice(cursor, at + 1));
56
- cursor = at + 1;
57
- continue;
58
- }
59
- let i = at + 1;
60
- while (i < text.length) {
61
- const ch = text[i];
62
- if (ch === undefined)
63
- break;
64
- if (ch === '\\' && text[i + 1] === ' ') {
65
- i += 2;
66
- continue;
67
- }
68
- if (isWhitespace(ch))
69
- break;
70
- i += 1;
71
- }
72
- const rawPath = text.slice(at + 1, i);
73
- const path = rawPath.replace(/\\ /g, ' ');
74
- if (rawPath.length > 0 && ATTACHMENT_EXT_RE.test(path)) {
75
- pieces.push(text.slice(cursor, at));
76
- hadAttachment = true;
77
- cursor = i;
78
- }
79
- else {
80
- pieces.push(text.slice(cursor, i));
81
- cursor = i;
82
- }
83
- }
84
- const stripped = pieces
85
- .join('')
86
- .replace(/[ \t]{2,}/g, ' ')
87
- .replace(/[ \t]+\n/g, '\n')
88
- .trim();
89
- return { stripped, hadAttachment };
90
- }
91
- function extractTaskKeys(text, issueId) {
92
- const seen = new Set();
93
- const out = [];
94
- const push = (raw) => {
95
- const key = raw.toUpperCase();
96
- if (seen.has(key))
97
- return;
98
- seen.add(key);
99
- out.push(key);
100
- };
101
- for (const m of text.matchAll(TASK_KEY_RE))
102
- push(m[0]);
103
- if (issueId && issueId.length > 0)
104
- push(issueId);
105
- return out;
106
- }
107
- function fileEditTokens(fileEdits) {
108
- if (!fileEdits || fileEdits.length === 0)
109
- return [];
110
- const seen = new Set();
111
- const out = [];
112
- const push = (raw) => {
113
- const tok = raw.toLowerCase();
114
- if (tok.length < 2 || seen.has(tok))
115
- return;
116
- seen.add(tok);
117
- out.push(tok);
118
- };
119
- for (const edit of fileEdits) {
120
- const base = edit.path.split(/[\\/]/).pop() ?? edit.path;
121
- for (const part of base.split(/[^a-zA-Z0-9]+/))
122
- push(part);
123
- for (const sym of edit.symbols ?? [])
124
- push(sym);
125
- }
126
- return out;
127
- }
128
- function normalizeDeducedTokens(deduced) {
129
- if (!deduced || deduced.length === 0)
130
- return [];
131
- const seen = new Set();
132
- const out = [];
133
- for (const raw of deduced) {
134
- const tok = raw.toLowerCase();
135
- if (tok.length < 2 || seen.has(tok))
136
- continue;
137
- seen.add(tok);
138
- out.push(tok);
139
- }
140
- return out;
141
- }
142
- export function buildRecallQuery(input) {
143
- const taskKeys = extractTaskKeys(input.text, input.issueId);
144
- const fileTokens = fileEditTokens(input.fileEdits);
145
- const contentTokens = distinctiveTokens(input.text);
146
- const deducedTokens = normalizeDeducedTokens(input.deducedTokens);
147
- const tier = taskKeys.length
148
- ? 'A'
149
- : fileTokens.length
150
- ? 'B'
151
- : contentTokens.length
152
- ? 'C'
153
- : deducedTokens.length
154
- ? 'D'
155
- : null;
156
- if (tier === null)
157
- return null;
158
- const queryParts = [...taskKeys, ...fileTokens, ...contentTokens, ...deducedTokens];
159
- const tokenSeen = new Set();
160
- const tokens = [];
161
- for (const part of [
162
- ...taskKeys.map((k) => k.toLowerCase()),
163
- ...fileTokens,
164
- ...contentTokens,
165
- ...deducedTokens,
166
- ]) {
167
- if (tokenSeen.has(part))
168
- continue;
169
- tokenSeen.add(part);
170
- tokens.push(part);
171
- }
172
- return { query: queryParts.join(' '), tokens, tier };
173
- }
174
- export const SIGNATURE_TOKEN_CAP = 40;
175
- function dedupe(tokens) {
176
- const seen = new Set();
177
- const out = [];
178
- for (const tok of tokens) {
179
- if (seen.has(tok))
180
- continue;
181
- seen.add(tok);
182
- out.push(tok);
183
- }
184
- return out;
185
- }
186
- export const MIN_SHIFT_NEW_TOKENS = 2;
187
- export function evaluateShift(signature, tokens) {
188
- const covered = new Set(Array.isArray(signature) ? signature : []);
189
- const newTokens = dedupe(tokens).filter((t) => !covered.has(t));
190
- return { shift: newTokens.length >= MIN_SHIFT_NEW_TOKENS, newTokens };
191
- }
192
- export const EMPTY_ASK_TOKEN_CAP = 200;
193
- export function askedAndEmpty(newTokens, emptyAsk) {
194
- if (!Array.isArray(emptyAsk) || emptyAsk.length === 0)
195
- return false;
196
- if (!Array.isArray(newTokens) || newTokens.length === 0)
197
- return false;
198
- const known = new Set(emptyAsk);
199
- return newTokens.every((t) => known.has(t));
200
- }
201
- export function foldEmptyAsk(emptyAsk, tokens) {
202
- const existing = Array.isArray(emptyAsk) ? emptyAsk : [];
203
- const merged = [...existing];
204
- const present = new Set(existing);
205
- for (const tok of tokens) {
206
- if (present.has(tok))
207
- continue;
208
- present.add(tok);
209
- merged.push(tok);
210
- }
211
- return merged.length > EMPTY_ASK_TOKEN_CAP
212
- ? merged.slice(merged.length - EMPTY_ASK_TOKEN_CAP)
213
- : merged;
214
- }
215
- export function commitSignature(signature, tokens) {
216
- const existing = Array.isArray(signature) ? signature : [];
217
- const merged = [...existing];
218
- const present = new Set(existing);
219
- for (const tok of tokens) {
220
- if (present.has(tok))
221
- continue;
222
- present.add(tok);
223
- merged.push(tok);
224
- }
225
- return merged.length > SIGNATURE_TOKEN_CAP
226
- ? merged.slice(merged.length - SIGNATURE_TOKEN_CAP)
227
- : merged;
228
- }
229
- export const PATH_STOPWORDS = new Set([
230
- 'packages', 'package', 'src', 'lib', 'libs', 'index', 'dist', 'build', 'out',
231
- 'node', 'modules', 'test', 'tests', 'spec', 'specs', 'mock', 'mocks',
232
- 'components', 'component', 'utils', 'util', 'helpers', 'helper', 'hooks',
233
- 'types', 'type', 'config', 'configs', 'scripts', 'script', 'app', 'apps',
234
- 'ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs', 'json', 'md', 'yml', 'yaml', 'css',
235
- 'new', 'old', 'tmp', 'temp', 'main', 'common', 'shared', 'core', 'base',
236
- 'file', 'files', 'data', 'docs', 'doc', 'public', 'assets', 'static',
237
- ]);
238
- export function basenameTokens(filePath) {
239
- const base = String(filePath).split(/[\\/]/).pop() ?? String(filePath);
240
- const dot = base.lastIndexOf('.');
241
- const stem = dot > 0 ? base.slice(0, dot) : base;
242
- const out = [];
243
- const seen = new Set();
244
- for (const raw of stem.toLowerCase().split(/[^a-z0-9]+/)) {
245
- if (raw.length < 3 || /^\d+$/.test(raw))
246
- continue;
247
- if (STOPWORDS.has(raw) || PATH_STOPWORDS.has(raw) || seen.has(raw))
248
- continue;
249
- seen.add(raw);
250
- out.push(raw);
251
- }
252
- return out;
253
- }
254
- export function changeTokens(text) {
255
- const out = new Set();
256
- for (const raw of String(text).toLowerCase().split(/[^a-z0-9]+/)) {
257
- if (raw.length < 4 || /^\d+$/.test(raw))
258
- continue;
259
- if (STOPWORDS.has(raw) || CODE_STOPWORDS.has(raw))
260
- continue;
261
- out.add(raw);
262
- }
263
- return out;
264
- }
265
- const CODE_STOPWORDS = new Set([
266
- 'const', 'let', 'var', 'function', 'return', 'import', 'export', 'from',
267
- 'type', 'types', 'interface', 'class', 'extends', 'implements', 'string',
268
- 'number', 'boolean', 'void', 'null', 'undefined', 'true', 'false', 'async',
269
- 'await', 'then', 'catch', 'throw', 'case', 'switch', 'break', 'continue',
270
- 'public', 'private', 'protected', 'readonly', 'static', 'this', 'that',
271
- 'with', 'when', 'where', 'which', 'while', 'each', 'into', 'over', 'they',
272
- 'them', 'their', 'there', 'these', 'those', 'than', 'been', 'being', 'have',
273
- 'has', 'had', 'does', 'done', 'will', 'would', 'could', 'should', 'must',
274
- ]);
275
- export const MIN_CHANGE_OVERLAP = 3;
276
- export function pathTokens(filePath) {
277
- const out = [];
278
- const seen = new Set();
279
- for (const raw of String(filePath).toLowerCase().split(/[^a-z0-9]+/)) {
280
- if (raw.length < 2)
281
- continue;
282
- if (STOPWORDS.has(raw) || PATH_STOPWORDS.has(raw) || seen.has(raw))
283
- continue;
284
- seen.add(raw);
285
- out.push(raw);
286
- }
287
- return out;
288
- }
@@ -1,47 +0,0 @@
1
- const MAX_LISTED = 5;
2
- const TITLE_WIDTH = 72;
3
- export function receiptCapable(agent) {
4
- return agent !== 'cursor';
5
- }
6
- function fitTitle(raw) {
7
- const flat = String(raw ?? '')
8
- .replace(/\s+/g, ' ')
9
- .trim();
10
- if (flat === '')
11
- return '';
12
- return flat.length > TITLE_WIDTH ? `${flat.slice(0, TITLE_WIDTH - 1)}…` : flat;
13
- }
14
- function detailRow(entry) {
15
- const kind = typeof entry.kind === 'string' ? entry.kind.trim() : '';
16
- const source = typeof entry.source === 'string' ? entry.source.trim() : '';
17
- return [kind, source].filter(Boolean).join(' · ');
18
- }
19
- export function renderReceipt(entries, opts = {}) {
20
- if (!Array.isArray(entries))
21
- return null;
22
- const rows = entries
23
- .filter((e) => e !== null && typeof e === 'object')
24
- .map((e) => ({
25
- n: Number.isInteger(e.n) && e.n > 0 ? e.n : null,
26
- title: fitTitle(e.title),
27
- detail: detailRow(e),
28
- }))
29
- .filter((e) => e.title !== '');
30
- if (rows.length === 0)
31
- return null;
32
- const noun = rows.length === 1 ? 'rule' : 'rules';
33
- const head = opts.delta
34
- ? `⚓ ${rows.length} new ${noun} for this turn`
35
- : `⚓ ${rows.length} ${noun} from your team`;
36
- const lines = [head];
37
- for (const row of rows.slice(0, MAX_LISTED)) {
38
- const handle = row.n === null ? ' ·' : `#${row.n}`.padStart(3);
39
- lines.push(` ${handle} ${row.title}`);
40
- if (row.detail !== '')
41
- lines.push(` ${row.detail}`);
42
- }
43
- const hidden = rows.length - MAX_LISTED;
44
- if (hidden > 0)
45
- lines.push(` + ${hidden} more`);
46
- return lines.join('\n');
47
- }