tryharness 0.7.22 → 0.7.25

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 (64) hide show
  1. package/README.md +48 -8
  2. package/assets/opencode-plugin.js +48 -10
  3. package/assets/pi-extension.ts +106 -26
  4. package/cli.mjs +1281 -289
  5. package/lib/activity.mjs +197 -40
  6. package/lib/adapters.mjs +337 -54
  7. package/lib/agentnode.mjs +175 -35
  8. package/lib/atlas.mjs +604 -0
  9. package/lib/bip39-english.txt +2048 -0
  10. package/lib/cards.mjs +177 -39
  11. package/lib/claudemem.mjs +74 -18
  12. package/lib/commands.mjs +13 -5
  13. package/lib/context.mjs +628 -0
  14. package/lib/control-client.mjs +5 -2
  15. package/lib/coordinator.mjs +281 -56
  16. package/lib/daemon.mjs +5590 -838
  17. package/lib/desktop.mjs +149 -35
  18. package/lib/digest.mjs +81 -0
  19. package/lib/environment.mjs +180 -0
  20. package/lib/files.mjs +246 -13
  21. package/lib/firewall.mjs +288 -0
  22. package/lib/folders.mjs +223 -57
  23. package/lib/frames.mjs +333 -0
  24. package/lib/inference.mjs +663 -0
  25. package/lib/learn.mjs +182 -0
  26. package/lib/live.mjs +286 -61
  27. package/lib/localmodels.mjs +571 -0
  28. package/lib/mcp-stdio.mjs +912 -103
  29. package/lib/media.mjs +228 -0
  30. package/lib/members.json +1 -1
  31. package/lib/members.mjs +35 -9
  32. package/lib/models.mjs +342 -80
  33. package/lib/network.mjs +201 -42
  34. package/lib/notify.mjs +49 -12
  35. package/lib/nudge.mjs +190 -32
  36. package/lib/ocr.mjs +380 -0
  37. package/lib/onboard.mjs +321 -70
  38. package/lib/parsers.mjs +132 -41
  39. package/lib/paths.mjs +33 -10
  40. package/lib/personal.mjs +77 -0
  41. package/lib/phones.mjs +398 -51
  42. package/lib/places.mjs +566 -102
  43. package/lib/provenance.mjs +19 -4
  44. package/lib/receipts.mjs +82 -0
  45. package/lib/redact.mjs +730 -0
  46. package/lib/runner.mjs +347 -55
  47. package/lib/runtime-install.mjs +59 -17
  48. package/lib/screens.mjs +212 -0
  49. package/lib/sealed.mjs +160 -28
  50. package/lib/secrets.mjs +18 -5
  51. package/lib/service.mjs +173 -35
  52. package/lib/snippets.mjs +140 -0
  53. package/lib/speech.mjs +993 -0
  54. package/lib/store.mjs +1678 -301
  55. package/lib/swarm.mjs +598 -117
  56. package/lib/t3.mjs +400 -79
  57. package/lib/trails.mjs +401 -0
  58. package/lib/ui.mjs +95 -23
  59. package/lib/vectors.mjs +5 -2
  60. package/lib/wakes.mjs +284 -68
  61. package/lib/watches.mjs +362 -0
  62. package/lib/worktrees.mjs +186 -38
  63. package/package.json +6 -4
  64. package/lib/embedder.mjs +0 -156
package/lib/cards.mjs CHANGED
@@ -14,96 +14,217 @@ import { choiceOf, describePick } from "./models.mjs";
14
14
 
15
15
  // How a member hears that work is waiting for it: how it is set up to be woken (its setup from the daemon, { state, how }),
16
16
  // else its roster wake.
17
- const ANSWERS = { run: "starts as soon as it may", email: "woken by email when work is posted", webhook: "woken when work is posted", "local-webhook": "woken when work is posted",
18
- schedule: "picks work up on its own schedule", none: "only when the person next talks to it" };
19
- export const answersFor = (entry) => entry.setup?.state === "not_set_up" ? "not set up yet: work posted for it waits until the person sets it up in Harness"
20
- : entry.setup?.state === "not_answering" ? "not answering right now: work posted for it waits until it does"
21
- : ANSWERS[entry.setup?.how ?? member(entry.id)?.wake] ?? "when it next looks";
17
+ const ANSWERS = {
18
+ run: "starts as soon as it may",
19
+ email: "woken by email when work is posted",
20
+ webhook: "woken when work is posted",
21
+ "local-webhook": "woken when work is posted",
22
+ schedule: "picks work up on its own schedule",
23
+ none: "only when the person next talks to it",
24
+ };
25
+ export const answersFor = (entry) =>
26
+ entry.setup?.state === "not_set_up"
27
+ ? "not set up yet: work posted for it waits until the person sets it up in Harness"
28
+ : entry.setup?.state === "not_answering"
29
+ ? "not answering right now: work posted for it waits until it does"
30
+ : (ANSWERS[entry.setup?.how ?? member(entry.id)?.wake] ??
31
+ "when it next looks");
22
32
 
23
33
  const NOTE_LIMIT = 300;
24
34
  const noteKey = (id) => `agent-note:${id}`;
25
35
  export const noteOf = (store, id) => store.meta(noteKey(id)) || null;
26
36
  /** The person's own words about one agent ("use it for reviews"); empty clears it. */
27
37
  export function setNote(store, id, text) {
28
- const note = String(text || "").replace(/\s+/g, " ").trim().slice(0, NOTE_LIMIT);
38
+ const note = String(text || "")
39
+ .replace(/\s+/g, " ")
40
+ .trim()
41
+ .slice(0, NOTE_LIMIT);
29
42
  store.meta(noteKey(id), note);
30
43
  return note || null;
31
44
  }
32
45
 
33
46
  // The model a coding agent's own settings name, as it runs here (the daemon passes no model of its own). Only that key
34
47
  // is read; null when the settings name none.
35
- const readText = (file) => { try { return fs.readFileSync(file, "utf8"); } catch { return null; } };
36
- const readJson = (file) => { const text = readText(file); if (text === null) return null; try { return JSON.parse(text.replace(/^\s*\/\/.*$/gm, "")); } catch { return null; } };
48
+ const readText = (file) => {
49
+ try {
50
+ return fs.readFileSync(file, "utf8");
51
+ } catch {
52
+ return null;
53
+ }
54
+ };
55
+ const readJson = (file) => {
56
+ const text = readText(file);
57
+ if (text === null) return null;
58
+ try {
59
+ return JSON.parse(text.replace(/^\s*\/\/.*$/gm, ""));
60
+ } catch {
61
+ return null;
62
+ }
63
+ };
37
64
  const MODELS = {
38
- "claude-code": (home) => readJson(path.join(home, ".claude", "settings.json"))?.model,
65
+ "claude-code": (home) =>
66
+ readJson(path.join(home, ".claude", "settings.json"))?.model,
39
67
  // Top-level keys come before the first [section].
40
- codex: (home) => readText(path.join(home, ".codex", "config.toml"))?.split(/^\s*\[/m)[0].match(/^\s*model\s*=\s*"([^"]+)"/m)?.[1],
41
- pi: (home) => { const settings = readJson(path.join(home, ".pi", "agent", "settings.json")); return settings?.defaultModel ? [settings.defaultProvider, settings.defaultModel].filter(Boolean).join("/") : null; },
42
- opencode: (home) => (readJson(path.join(home, ".config", "opencode", "opencode.json")) ?? readJson(path.join(home, ".config", "opencode", "opencode.jsonc")))?.model,
68
+ codex: (home) =>
69
+ readText(path.join(home, ".codex", "config.toml"))
70
+ ?.split(/^\s*\[/m)[0]
71
+ .match(/^\s*model\s*=\s*"([^"]+)"/m)?.[1],
72
+ pi: (home) => {
73
+ const settings = readJson(path.join(home, ".pi", "agent", "settings.json"));
74
+ return settings?.defaultModel
75
+ ? [settings.defaultProvider, settings.defaultModel]
76
+ .filter(Boolean)
77
+ .join("/")
78
+ : null;
79
+ },
80
+ opencode: (home) =>
81
+ (
82
+ readJson(path.join(home, ".config", "opencode", "opencode.json")) ??
83
+ readJson(path.join(home, ".config", "opencode", "opencode.jsonc"))
84
+ )?.model,
43
85
  };
44
86
  export function modelOf(id, home = os.homedir()) {
45
87
  const found = MODELS[id]?.(home);
46
- return typeof found === "string" && found.trim() ? found.trim().slice(0, 100) : null;
88
+ return typeof found === "string" && found.trim()
89
+ ? found.trim().slice(0, 100)
90
+ : null;
47
91
  }
48
92
 
49
93
  // The person's choice in the relay (models.mjs), else what the agent's own settings name.
50
94
  const modelLine = (store, id, home) => {
51
95
  const choice = choiceOf(store, id);
52
- return choice?.auto ? "picked for each run by Auto" : choice ? describePick(choice) : modelOf(id, home);
96
+ return choice?.auto
97
+ ? "picked for each run by Auto"
98
+ : choice
99
+ ? describePick(choice)
100
+ : modelOf(id, home);
53
101
  };
54
102
 
55
103
  const RECENT = 50;
56
- const median = (values) => { const sorted = [...values].sort((a, b) => a - b); return sorted.length ? sorted[Math.floor(sorted.length / 2)] : null; };
104
+ const median = (values) => {
105
+ const sorted = [...values].sort((a, b) => a - b);
106
+ return sorted.length ? sorted[Math.floor(sorted.length / 2)] : null;
107
+ };
57
108
 
58
109
  /** Cards for the given roster entries ({ id, setup? }), in their order. A card's setup is its state alone ("not_set_up"). */
59
110
  export function swarmCards(store, entries, { home } = {}) {
60
111
  const work = new Map();
61
112
  for (const row of store.agentWork()) {
62
- const entry = work.get(row.agent) ?? { done: 0, failed: 0, minutes: [], working: 0, waiting: 0 };
113
+ const entry = work.get(row.agent) ?? {
114
+ done: 0,
115
+ failed: 0,
116
+ minutes: [],
117
+ working: 0,
118
+ waiting: 0,
119
+ };
63
120
  if (row.status === "done") {
64
121
  entry.done++;
65
122
  // Minutes from when it could start (the person's Start, else the post) to its report, over its latest work.
66
- const minutes = (Date.parse(row.reported_at) - Date.parse(row.started_at)) / 60_000;
67
- if (entry.minutes.length < RECENT && Number.isFinite(minutes) && minutes >= 0) entry.minutes.push(minutes);
123
+ const minutes =
124
+ (Date.parse(row.reported_at) - Date.parse(row.started_at)) / 60_000;
125
+ if (
126
+ entry.minutes.length < RECENT &&
127
+ Number.isFinite(minutes) &&
128
+ minutes >= 0
129
+ )
130
+ entry.minutes.push(minutes);
68
131
  } else if (row.status === "failed") entry.failed++;
69
- else if (row.status === "claimed" || row.status === "running") entry.working++;
132
+ else if (row.status === "claimed" || row.status === "running")
133
+ entry.working++;
70
134
  else if (row.status === "open" && row.targeted) entry.waiting++;
71
135
  work.set(row.agent, entry);
72
136
  }
73
- const seen = new Map(store.agentsSeen().map((row) => [row.agent, row.last_seen_at]));
137
+ const seen = new Map(
138
+ store.agentsSeen().map((row) => [row.agent, row.last_seen_at])
139
+ );
140
+ // What its own sessions showed after its changes, from the relay's receipts (a store from before receipts has none).
141
+ const checks = store.receiptCounts?.() ?? {};
74
142
  return entries.map((entry) => {
75
143
  const roster = member(entry.id) ?? {};
76
- const done = work.get(entry.id) ?? { done: 0, failed: 0, minutes: [], working: 0, waiting: 0 };
144
+ const done = work.get(entry.id) ?? {
145
+ done: 0,
146
+ failed: 0,
147
+ minutes: [],
148
+ working: 0,
149
+ waiting: 0,
150
+ };
77
151
  const minutes = median(done.minutes);
78
152
  return {
79
- id: entry.id, name: roster.name ?? entry.id, about: roster.about ?? "", limits: roster.limits ?? null, where: roster.where ?? null,
80
- ...(roster.runs === "machine" ? { review: roster.review === true, model: modelLine(store, entry.id, home) } : {}),
153
+ id: entry.id,
154
+ name: roster.name ?? entry.id,
155
+ about: roster.about ?? "",
156
+ limits: roster.limits ?? null,
157
+ where: roster.where ?? null,
158
+ ...(roster.runs === "machine"
159
+ ? {
160
+ review: roster.review === true,
161
+ model: modelLine(store, entry.id, home),
162
+ }
163
+ : {}),
81
164
  answers: answersFor(entry),
82
- record: { done: done.done, failed: done.failed, medianMinutes: minutes === null ? null : Math.round(minutes * 10) / 10 },
83
- working: done.working, waiting: done.waiting, lastSeen: seen.get(entry.id) ?? null, note: noteOf(store, entry.id),
165
+ record: {
166
+ done: done.done,
167
+ failed: done.failed,
168
+ medianMinutes: minutes === null ? null : Math.round(minutes * 10) / 10,
169
+ ...(checks[entry.id] ? { checks: checks[entry.id] } : {}),
170
+ },
171
+ working: done.working,
172
+ waiting: done.waiting,
173
+ lastSeen: seen.get(entry.id) ?? null,
174
+ note: noteOf(store, entry.id),
84
175
  ...(entry.setup?.state ? { setup: String(entry.setup.state) } : {}),
85
176
  };
86
177
  });
87
178
  }
88
179
 
89
- const span = (minutes) => minutes < 1 ? "under a minute" : minutes < 60 ? `${Math.round(minutes)} min`
90
- : minutes < 48 * 60 ? `${Math.floor(minutes / 60)} h${Math.round(minutes % 60) ? ` ${Math.round(minutes % 60)} min` : ""}` : `${Math.round(minutes / 1_440)} days`;
91
- const sentence = (text) => `${text.charAt(0).toUpperCase()}${text.slice(1)}${/[.!?]$/.test(text) ? "" : "."}`;
180
+ const span = (minutes) =>
181
+ minutes < 1
182
+ ? "under a minute"
183
+ : minutes < 60
184
+ ? `${Math.round(minutes)} min`
185
+ : minutes < 48 * 60
186
+ ? `${Math.floor(minutes / 60)} h${Math.round(minutes % 60) ? ` ${Math.round(minutes % 60)} min` : ""}`
187
+ : `${Math.round(minutes / 1_440)} days`;
188
+ const sentence = (text) =>
189
+ `${text.charAt(0).toUpperCase()}${text.slice(1)}${/[.!?]$/.test(text) ? "" : "."}`;
92
190
 
93
191
  /** One card as one line of plain text. */
94
192
  export function cardLine(card, now = Date.now()) {
95
193
  const parts = [`@${card.id} (${card.name}): ${sentence(card.about)}`];
96
194
  if (card.limits) parts.push(card.limits);
97
- if (card.review !== undefined) parts.push(card.review ? "Can review without changing files." : "Has no review-only mode: its runs may change files.");
98
- if (card.where) parts.push(`Runs ${card.where}${card.model ? `, model ${card.model}` : ""}.`);
195
+ if (card.review !== undefined)
196
+ parts.push(
197
+ card.review
198
+ ? "Can review without changing files."
199
+ : "Has no review-only mode: its runs may change files."
200
+ );
201
+ if (card.where)
202
+ parts.push(
203
+ `Runs ${card.where}${card.model ? `, model ${card.model}` : ""}.`
204
+ );
99
205
  parts.push(sentence(card.answers));
100
206
  const { done, failed, medianMinutes } = card.record;
101
207
  // Work for an agent whose tasks live on Harness's server is not on this board, so none here says nothing about it.
102
- if (done || failed) parts.push(`On this board: ${done} done, ${failed} failed${medianMinutes === null ? "" : `; a median of ${span(medianMinutes)} from start to report`}.`);
103
- else if (member(card.id)?.home === "machine") parts.push("Has finished no work on this board yet.");
104
- if (card.working || card.waiting) parts.push(`Now: ${card.working} running, ${card.waiting} waiting for it.`);
208
+ if (done || failed)
209
+ parts.push(
210
+ `On this board: ${done} done, ${failed} failed${medianMinutes === null ? "" : `; a median of ${span(medianMinutes)} from start to report`}.`
211
+ );
212
+ const checks = card.record.checks;
213
+ if (checks)
214
+ parts.push(
215
+ `Its own sessions show its changes tested after the last edit ${checks.tested} of ${checks.tested + checks.untested + checks.failing} times${checks.failing ? ` (${checks.failing} with tests failing)` : ""}.`
216
+ );
217
+ else if (member(card.id)?.home === "machine")
218
+ parts.push("Has finished no work on this board yet.");
219
+ if (card.working || card.waiting)
220
+ parts.push(`Now: ${card.working} running, ${card.waiting} waiting for it.`);
105
221
  // An agent that picks work up itself: when it last looked says whether it is around.
106
- if (member(card.id)?.wake !== "run") parts.push(card.lastSeen ? `Last checked the board ${span((now - Date.parse(card.lastSeen)) / 60_000)} ago.` : "Has not checked the board yet.");
222
+ if (member(card.id)?.wake !== "run")
223
+ parts.push(
224
+ card.lastSeen
225
+ ? `Last checked the board ${span((now - Date.parse(card.lastSeen)) / 60_000)} ago.`
226
+ : "Has not checked the board yet."
227
+ );
107
228
  if (card.note) parts.push(`The person says: ${sentence(card.note)}`);
108
229
  return `- ${parts.join(" ")}`;
109
230
  }
@@ -111,11 +232,28 @@ export function cardLine(card, now = Date.now()) {
111
232
  /** The block an agent is told when its session starts: every other agent's card. */
112
233
  export function swarmBlock(cards, now = Date.now()) {
113
234
  if (!cards.length) return "";
114
- return ["Your swarm, the person's other agents. Hand one work with post_task (target_agent) or ask one in a thread with post_to_room (ask).",
115
- ...cards.map((card) => cardLine(card, now))].join("\n");
235
+ return [
236
+ "Your swarm, the person's other agents. Hand one work with post_task (target_agent) or ask one in a thread with post_to_room (ask).",
237
+ ...cards.map((card) => cardLine(card, now)),
238
+ ].join("\n");
116
239
  }
117
240
 
118
241
  /** What the block says that stays put (who is in the swarm, their facts, the person's notes), so it is told again only when that changes. */
119
- export const swarmDigest = (cards) => createHash("sha256")
120
- .update(JSON.stringify(cards.map((card) => [card.id, card.about, card.limits, card.where, card.review ?? null, card.model ?? null, card.note, card.setup ?? null])))
121
- .digest("hex").slice(0, 16);
242
+ export const swarmDigest = (cards) =>
243
+ createHash("sha256")
244
+ .update(
245
+ JSON.stringify(
246
+ cards.map((card) => [
247
+ card.id,
248
+ card.about,
249
+ card.limits,
250
+ card.where,
251
+ card.review ?? null,
252
+ card.model ?? null,
253
+ card.note,
254
+ card.setup ?? null,
255
+ ])
256
+ )
257
+ )
258
+ .digest("hex")
259
+ .slice(0, 16);
package/lib/claudemem.mjs CHANGED
@@ -12,13 +12,22 @@ import { spawnSync } from "node:child_process";
12
12
 
13
13
  function readSettings(env) {
14
14
  const dir = env.CLAUDE_MEM_DATA_DIR || path.join(os.homedir(), ".claude-mem");
15
- try { return JSON.parse(fs.readFileSync(path.join(dir, "settings.json"), "utf8")); } catch { return null; }
15
+ try {
16
+ return JSON.parse(fs.readFileSync(path.join(dir, "settings.json"), "utf8"));
17
+ } catch {
18
+ return null;
19
+ }
16
20
  }
17
21
 
18
- const setting = (env, settings, key, fallback) => env[key] || settings[key] || fallback;
22
+ const setting = (env, settings, key, fallback) =>
23
+ env[key] || settings[key] || fallback;
19
24
 
20
25
  function git(cwd, args) {
21
- const result = spawnSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
26
+ const result = spawnSync("git", args, {
27
+ cwd,
28
+ encoding: "utf8",
29
+ stdio: ["ignore", "pipe", "ignore"],
30
+ });
22
31
  return result.status === 0 ? result.stdout.trim() || null : null;
23
32
  }
24
33
 
@@ -26,15 +35,30 @@ function git(cwd, args) {
26
35
  function remoteSlug(url) {
27
36
  if (!url || /^(file:|[/.~\\]|[a-z]:[\\/])/i.test(url)) return null;
28
37
  const cleaned = url.replace(/\/+$/, "").replace(/\.git$/i, "");
29
- const tail = cleaned.match(/^[a-z][a-z0-9+.-]*:\/\/[^/]+\/(.+)$/i)?.[1] ?? cleaned.match(/^(?:[^/@\s]+@)?[^:/\s]+:(?!\/\/)(.+)$/)?.[1];
30
- const segments = (tail || "").split("/").filter((segment) => segment && segment !== "_git");
38
+ const tail =
39
+ cleaned.match(/^[a-z][a-z0-9+.-]*:\/\/[^/]+\/(.+)$/i)?.[1] ??
40
+ cleaned.match(/^(?:[^/@\s]+@)?[^:/\s]+:(?!\/\/)(.+)$/)?.[1];
41
+ const segments = (tail || "")
42
+ .split("/")
43
+ .filter((segment) => segment && segment !== "_git");
31
44
  return segments.length ? segments.slice(-2).join("/") : null;
32
45
  }
33
46
 
34
47
  function markerRoot(dir) {
35
- const stops = new Set([os.homedir(), os.tmpdir(), "/"].map((stop) => path.resolve(stop)));
36
- for (let current = path.resolve(dir); !stops.has(current); current = path.dirname(current)) {
37
- if ([".claude-mem-project", ".claude-mem.json"].some((marker) => fs.existsSync(path.join(current, marker)))) return current;
48
+ const stops = new Set(
49
+ [os.homedir(), os.tmpdir(), "/"].map((stop) => path.resolve(stop))
50
+ );
51
+ for (
52
+ let current = path.resolve(dir);
53
+ !stops.has(current);
54
+ current = path.dirname(current)
55
+ ) {
56
+ if (
57
+ [".claude-mem-project", ".claude-mem.json"].some((marker) =>
58
+ fs.existsSync(path.join(current, marker))
59
+ )
60
+ )
61
+ return current;
38
62
  if (path.dirname(current) === current) break;
39
63
  }
40
64
  return null;
@@ -45,33 +69,65 @@ export function projectNames(cwd, env, settings) {
45
69
  const root = git(cwd, ["rev-parse", "--show-toplevel"]);
46
70
  if (!root) return [path.basename(markerRoot(cwd) || cwd)];
47
71
  const names = [];
48
- if (String(setting(env, settings, "CLAUDE_MEM_PROJECT_NAME_SOURCE", "path")).toLowerCase() === "git-remote") names.push(remoteSlug(git(root, ["remote", "get-url", "origin"])));
72
+ if (
73
+ String(
74
+ setting(env, settings, "CLAUDE_MEM_PROJECT_NAME_SOURCE", "path")
75
+ ).toLowerCase() === "git-remote"
76
+ )
77
+ names.push(remoteSlug(git(root, ["remote", "get-url", "origin"])));
49
78
  names.push(path.basename(root));
50
79
  // A worktree's memory can sit under the main checkout's name.
51
- const common = git(cwd, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
52
- if (common && path.basename(common) === ".git") names.push(path.basename(path.dirname(common)));
80
+ const common = git(cwd, [
81
+ "rev-parse",
82
+ "--path-format=absolute",
83
+ "--git-common-dir",
84
+ ]);
85
+ if (common && path.basename(common) === ".git")
86
+ names.push(path.basename(path.dirname(common)));
53
87
  return [...new Set(names.filter(Boolean))];
54
88
  }
55
89
 
56
90
  function render(observation) {
57
91
  const heading = `- ${String(observation.created_at).slice(0, 10)} · ${observation.type} · ${observation.title || "untitled"}`;
58
- return [heading, observation.subtitle, observation.narrative].filter(Boolean).join("\n ");
92
+ return [heading, observation.subtitle, observation.narrative]
93
+ .filter(Boolean)
94
+ .join("\n ");
59
95
  }
60
96
 
61
97
  /** claude-mem's newest notes on the project `cwd` belongs to, or null when it has none or is not running. */
62
- export async function claudeMemNotes(cwd, { env = process.env, timeoutMs, maxChars = 20_000 } = {}) {
98
+ export async function claudeMemNotes(
99
+ cwd,
100
+ { env = process.env, timeoutMs, maxChars = 20_000 } = {}
101
+ ) {
63
102
  const settings = cwd && readSettings(env);
64
103
  if (!settings) return null;
65
104
  const host = setting(env, settings, "CLAUDE_MEM_WORKER_HOST", "127.0.0.1");
66
- const port = setting(env, settings, "CLAUDE_MEM_WORKER_PORT", String(37700 + ((process.getuid?.() ?? 77) % 100)));
105
+ const port = setting(
106
+ env,
107
+ settings,
108
+ "CLAUDE_MEM_WORKER_PORT",
109
+ String(37700 + ((process.getuid?.() ?? 77) % 100))
110
+ );
67
111
  const base = `http://${host.includes(":") ? `[${host}]` : host}:${port}`;
68
112
  const signal = AbortSignal.timeout(timeoutMs);
69
113
  try {
70
- const known = new Set(((await (await fetch(`${base}/api/projects`, { signal })).json()).projects || []).map((name) => String(name).toLowerCase()));
71
- const project = projectNames(cwd, env, settings).find((name) => known.has(name.toLowerCase()));
114
+ const known = new Set(
115
+ (
116
+ (await (await fetch(`${base}/api/projects`, { signal })).json())
117
+ .projects || []
118
+ ).map((name) => String(name).toLowerCase())
119
+ );
120
+ const project = projectNames(cwd, env, settings).find((name) =>
121
+ known.has(name.toLowerCase())
122
+ );
72
123
  if (!project) return null;
73
- const limit = Number(setting(env, settings, "CLAUDE_MEM_CONTEXT_OBSERVATIONS", "50")) || 50;
74
- const response = await fetch(`${base}/api/search?type=observations&format=json&project=${encodeURIComponent(project)}&limit=${limit}`, { signal });
124
+ const limit =
125
+ Number(setting(env, settings, "CLAUDE_MEM_CONTEXT_OBSERVATIONS", "50")) ||
126
+ 50;
127
+ const response = await fetch(
128
+ `${base}/api/search?type=observations&format=json&project=${encodeURIComponent(project)}&limit=${limit}`,
129
+ { signal }
130
+ );
75
131
  if (!response.ok) return null;
76
132
  // Whole notes, newest first, as many as fit the task context's entry size; a note is never cut.
77
133
  const header = `Project "${project}", newest first:`;
package/lib/commands.mjs CHANGED
@@ -4,14 +4,22 @@ import { spawnSync } from "node:child_process";
4
4
 
5
5
  export function resolveCommand(command, env = process.env) {
6
6
  const pathValue = env.PATH || "";
7
- const extensions = process.platform === "win32"
8
- ? (env.PATHEXT || ".EXE;.CMD;.BAT;.COM").split(";")
9
- : [""];
7
+ const extensions =
8
+ process.platform === "win32"
9
+ ? (env.PATHEXT || ".EXE;.CMD;.BAT;.COM").split(";")
10
+ : [""];
10
11
  for (const dir of pathValue.split(path.delimiter).filter(Boolean)) {
11
12
  for (const extension of extensions) {
12
- const candidate = path.join(dir, process.platform === "win32" ? command + extension.toLowerCase() : command);
13
+ const candidate = path.join(
14
+ dir,
15
+ process.platform === "win32"
16
+ ? command + extension.toLowerCase()
17
+ : command
18
+ );
13
19
  const alternate = path.join(dir, command + extension);
14
- for (const file of candidate === alternate ? [candidate] : [candidate, alternate]) {
20
+ for (const file of candidate === alternate
21
+ ? [candidate]
22
+ : [candidate, alternate]) {
15
23
  try {
16
24
  fs.accessSync(file, fs.constants.X_OK);
17
25
  return file;