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.
- package/README.md +48 -8
- package/assets/opencode-plugin.js +48 -10
- package/assets/pi-extension.ts +106 -26
- package/cli.mjs +1281 -289
- package/lib/activity.mjs +197 -40
- package/lib/adapters.mjs +337 -54
- package/lib/agentnode.mjs +175 -35
- package/lib/atlas.mjs +604 -0
- package/lib/bip39-english.txt +2048 -0
- package/lib/cards.mjs +177 -39
- package/lib/claudemem.mjs +74 -18
- package/lib/commands.mjs +13 -5
- package/lib/context.mjs +628 -0
- package/lib/control-client.mjs +5 -2
- package/lib/coordinator.mjs +281 -56
- package/lib/daemon.mjs +5590 -838
- package/lib/desktop.mjs +149 -35
- package/lib/digest.mjs +81 -0
- package/lib/environment.mjs +180 -0
- package/lib/files.mjs +246 -13
- package/lib/firewall.mjs +288 -0
- package/lib/folders.mjs +223 -57
- package/lib/frames.mjs +333 -0
- package/lib/inference.mjs +663 -0
- package/lib/learn.mjs +182 -0
- package/lib/live.mjs +286 -61
- package/lib/localmodels.mjs +571 -0
- package/lib/mcp-stdio.mjs +912 -103
- package/lib/media.mjs +228 -0
- package/lib/members.json +1 -1
- package/lib/members.mjs +35 -9
- package/lib/models.mjs +342 -80
- package/lib/network.mjs +201 -42
- package/lib/notify.mjs +49 -12
- package/lib/nudge.mjs +190 -32
- package/lib/ocr.mjs +380 -0
- package/lib/onboard.mjs +321 -70
- package/lib/parsers.mjs +132 -41
- package/lib/paths.mjs +33 -10
- package/lib/personal.mjs +77 -0
- package/lib/phones.mjs +398 -51
- package/lib/places.mjs +566 -102
- package/lib/provenance.mjs +19 -4
- package/lib/receipts.mjs +82 -0
- package/lib/redact.mjs +730 -0
- package/lib/runner.mjs +347 -55
- package/lib/runtime-install.mjs +59 -17
- package/lib/screens.mjs +212 -0
- package/lib/sealed.mjs +160 -28
- package/lib/secrets.mjs +18 -5
- package/lib/service.mjs +173 -35
- package/lib/snippets.mjs +140 -0
- package/lib/speech.mjs +993 -0
- package/lib/store.mjs +1678 -301
- package/lib/swarm.mjs +598 -117
- package/lib/t3.mjs +400 -79
- package/lib/trails.mjs +401 -0
- package/lib/ui.mjs +95 -23
- package/lib/vectors.mjs +5 -2
- package/lib/wakes.mjs +284 -68
- package/lib/watches.mjs +362 -0
- package/lib/worktrees.mjs +186 -38
- package/package.json +6 -4
- 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 = {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
:
|
|
21
|
-
:
|
|
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 || "")
|
|
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) => {
|
|
36
|
-
|
|
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) =>
|
|
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) =>
|
|
41
|
-
|
|
42
|
-
|
|
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()
|
|
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
|
|
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) => {
|
|
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) ?? {
|
|
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 =
|
|
67
|
-
|
|
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")
|
|
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(
|
|
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) ?? {
|
|
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,
|
|
80
|
-
|
|
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: {
|
|
83
|
-
|
|
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) =>
|
|
90
|
-
|
|
91
|
-
|
|
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)
|
|
98
|
-
|
|
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)
|
|
103
|
-
|
|
104
|
-
|
|
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")
|
|
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 [
|
|
115
|
-
|
|
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) =>
|
|
120
|
-
|
|
121
|
-
|
|
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 {
|
|
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) =>
|
|
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, {
|
|
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 =
|
|
30
|
-
|
|
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(
|
|
36
|
-
|
|
37
|
-
|
|
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 (
|
|
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, [
|
|
52
|
-
|
|
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]
|
|
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(
|
|
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(
|
|
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(
|
|
71
|
-
|
|
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 =
|
|
74
|
-
|
|
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 =
|
|
8
|
-
|
|
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(
|
|
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
|
|
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;
|