arcroom 0.0.3-beta → 0.0.5-beta
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 +7 -3
- package/dist/code.js +34 -3
- package/dist/daemon.js +57 -17
- package/dist/doctor.js +6 -18
- package/dist/done.js +21 -9
- package/dist/events.js +7 -3
- package/dist/fresh.js +32 -8
- package/dist/git.js +22 -1
- package/dist/home.js +8 -3
- package/dist/hook.js +44 -18
- package/dist/index.js +50 -29
- package/dist/init.js +199 -20
- package/dist/lookup.js +1 -1
- package/dist/loop.js +13 -5
- package/dist/mcp.js +33 -10
- package/dist/pending.js +47 -0
- package/dist/recall.js +36 -12
- package/dist/redact.js +6 -0
- package/dist/scope.js +49 -0
- package/dist/sent.js +62 -0
- package/dist/session.js +29 -21
- package/dist/signs.js +10 -0
- package/dist/tree.js +40 -16
- package/dist/usage.js +14 -0
- package/package.json +1 -1
- package/dist/blind.js +0 -216
package/dist/hook.js
CHANGED
|
@@ -3,16 +3,17 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } fr
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { AGENTS, fromAgent, toAgent } from './agents.js';
|
|
5
5
|
import { ensureDaemon } from './daemon.js';
|
|
6
|
-
import { noteEdit, onStop } from './done.js';
|
|
6
|
+
import { EDIT_TOOLS, noteEdit, onStop, SHELL_TOOLS } from './done.js';
|
|
7
7
|
import { toArcroomEvent } from './events.js';
|
|
8
|
-
import { noteDir } from './code.js';
|
|
8
|
+
import { editClash, noteDir } from './code.js';
|
|
9
9
|
import { fence } from './fence.js';
|
|
10
10
|
import { redact, rememberSecrets } from './redact.js';
|
|
11
|
-
import { ARCROOM_HOME, isOff, keyRefused } from './home.js';
|
|
11
|
+
import { ARCROOM_HOME, isOff, keyRefused, loadConfig } from './home.js';
|
|
12
12
|
import { lookupFast } from './lookup.js';
|
|
13
13
|
import { checkLoop, sessionFile } from './loop.js';
|
|
14
14
|
import { pack } from './pack.js';
|
|
15
15
|
import { lastRunNote, projectOf } from './session.js';
|
|
16
|
+
import { laptopHookActs } from './scope.js';
|
|
16
17
|
import { bump, MARK, signFor, toolResult } from './signs.js';
|
|
17
18
|
import { costSinceClaim, markClaim, turnUsage } from './usage.js';
|
|
18
19
|
// Claude Code runs this for every hook event, with the event JSON on stdin.
|
|
@@ -43,8 +44,14 @@ export async function hook() {
|
|
|
43
44
|
const raw = fromAgent(JSON.parse(input), agent);
|
|
44
45
|
if (isOff(raw.cwd))
|
|
45
46
|
return; // switched off for this repo by this person (arcroom off): nothing recorded or sent
|
|
46
|
-
if (
|
|
47
|
-
return; //
|
|
47
|
+
if (process.argv.includes('--user') && !laptopHookActs(raw.cwd))
|
|
48
|
+
return; // laptop-wide hook: only set-up repos, never twice
|
|
49
|
+
if (keyRefused(loadConfig(raw.cwd))) {
|
|
50
|
+
// the key was revoked or its account deleted: nothing runs until a new key (arcroom init); said once, at session start
|
|
51
|
+
if (raw.hook_event_name === 'SessionStart')
|
|
52
|
+
process.stdout.write(JSON.stringify(toAgent(agent, 'SessionStart', { systemMessage: `${MARK} · stopped: the API refused this laptop's key. Run arcroom init with a new key (web app, Settings → API keys).` }, String(raw.session_id))));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
48
55
|
rememberSecrets(raw.cwd); // this project's .env values are redacted wherever they show up
|
|
49
56
|
const name = raw.hook_event_name;
|
|
50
57
|
const session = String(raw.session_id);
|
|
@@ -58,7 +65,7 @@ export async function hook() {
|
|
|
58
65
|
const injected = (kind, text, keepText = false) => emit('context', { kind, chars: text.length, ...(keepText ? { text: text.slice(0, 2000) } : {}) });
|
|
59
66
|
// Token meter: the turn's usage goes in before its turn.end, so the report built at turn.end includes it.
|
|
60
67
|
// After each tool call too, so the website shows a long turn's tokens while it runs.
|
|
61
|
-
if (name === 'Stop' || name === 'SessionEnd' || name === '
|
|
68
|
+
if (name === 'Stop' || name === 'SessionEnd' || name === 'PreToolUse') {
|
|
62
69
|
const usage = turnUsage(session, raw.transcript_path);
|
|
63
70
|
if (usage)
|
|
64
71
|
emit('usage', usage);
|
|
@@ -149,21 +156,27 @@ export async function hook() {
|
|
|
149
156
|
}
|
|
150
157
|
if (pointer)
|
|
151
158
|
given.add(pointer.memoId);
|
|
152
|
-
// what `arcroom wrong` reports when the person says the memory just handed over was wrong
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
159
|
+
// what `arcroom wrong` reports when the person says the memory just handed over was wrong: every memory handed over
|
|
160
|
+
// (steps or answer, playbook, each fact and decision), so it can name the one that was wrong
|
|
161
|
+
const handed = [
|
|
162
|
+
...(pointer ? [{ memo_id: pointer.memoId, kind: pointer.kind, text: pointer.intent }] : []),
|
|
163
|
+
...(playbook ? [{ memo_id: playbook.memoId, kind: 'playbook', text: playbook.intent ?? 'playbook' }] : []),
|
|
164
|
+
...(knowledge ? knowledge.items.map((i) => ({ memo_id: i.id, kind: i.kind, text: i.text })) : []),
|
|
165
|
+
];
|
|
166
|
+
if (handed.length)
|
|
167
|
+
writeFileSync(join(ARCROOM_HOME, 'last-recall.json'), JSON.stringify({ memo_id: handed[0].memo_id, intent: handed[0].text, handed }));
|
|
156
168
|
if (pointer || knowledge || playbook)
|
|
157
169
|
writeFileSync(givenFile, JSON.stringify([...given]));
|
|
158
170
|
if (pointer) {
|
|
159
171
|
injected('recall', pointer.text, true);
|
|
160
172
|
// the turn's outcome credits (or heals) these steps when the turn ends
|
|
173
|
+
const match = pointer.match ? { similar: true, match: pointer.match } : {};
|
|
161
174
|
if (pointer.kind === 'steps')
|
|
162
|
-
emit('memory.replayed', { memo_id: pointer.memoId });
|
|
175
|
+
emit('memory.replayed', { memo_id: pointer.memoId, ...match });
|
|
163
176
|
else
|
|
164
|
-
emit('memory.reused', { memo_id: pointer.memoId, version: pointer.version });
|
|
177
|
+
emit('memory.reused', { memo_id: pointer.memoId, version: pointer.version, ...match });
|
|
165
178
|
bump(session, pointer.kind === 'steps' ? 'replayed' : 'reused');
|
|
166
|
-
say(`${MARK} · ${pointer.kind === 'steps' ? 'steps from a past run' : 'a team answer'}${pointer.similar ? ' for a similar task' : ''}: ${pointer.intent.slice(0, 70)}`);
|
|
179
|
+
say(`${MARK} · ${pointer.kind === 'steps' ? 'steps from a past run' : 'a team answer'}${pointer.similar ? ' for a similar task' : ''}: ${pointer.intent.slice(0, 70)}${pointer.match ? ` (match: ${pointer.match})` : ''}`);
|
|
167
180
|
}
|
|
168
181
|
// a hit is the lookup already done: the reminder would only send the agent to find_task for it again
|
|
169
182
|
// what teammates' agents wrote goes in one fenced block of data (fence.ts); ArcRoom's own reminder stays outside it
|
|
@@ -184,11 +197,22 @@ export async function hook() {
|
|
|
184
197
|
say(`${MARK} · blocked a repeated ${String(raw.tool_name).replace(OUR_TOOLS, '')} call (same input 3 times in a row)`);
|
|
185
198
|
bump(session, 'blocked');
|
|
186
199
|
}
|
|
187
|
-
else
|
|
188
|
-
|
|
200
|
+
else {
|
|
201
|
+
// two agents on one file: the first edit waits once, with who is on it (the same edit again goes through)
|
|
202
|
+
const clash = EDIT_TOOLS.has(raw.tool_name)
|
|
203
|
+
? await editClash(loadConfig(raw.cwd), session, raw.cwd, raw.tool_input?.file_path ?? raw.tool_input?.notebook_path).catch(() => undefined)
|
|
204
|
+
: undefined;
|
|
205
|
+
if (clash) {
|
|
206
|
+
out.hookSpecificOutput = { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: clash };
|
|
207
|
+
injected('deny', clash);
|
|
208
|
+
say(`${MARK} · another agent is editing this file: asked yours to check first`);
|
|
209
|
+
}
|
|
210
|
+
else
|
|
211
|
+
noteEdit(raw);
|
|
212
|
+
}
|
|
189
213
|
}
|
|
190
214
|
// a done-check the agent ran with Bash (the Stop hook asks for them; it never runs them itself)
|
|
191
|
-
if (name === 'PostToolUse' && raw.tool_name
|
|
215
|
+
if (name === 'PostToolUse' && SHELL_TOOLS.has(raw.tool_name))
|
|
192
216
|
noteEdit(raw);
|
|
193
217
|
if (name === 'PostToolUse' && String(raw.tool_name).startsWith(OUR_TOOLS)) {
|
|
194
218
|
const tool = raw.tool_name.slice(OUR_TOOLS.length);
|
|
@@ -227,14 +251,16 @@ export async function hook() {
|
|
|
227
251
|
}
|
|
228
252
|
if (project) {
|
|
229
253
|
// Session memory: start with what the last run in this project did. Slow or offline API = no note.
|
|
230
|
-
const
|
|
254
|
+
const root = git(['rev-parse', '--show-toplevel'], { cwd: raw.cwd }).stdout?.trim();
|
|
255
|
+
const note = await lastRunNote(session, project, branch, root).catch(() => undefined);
|
|
231
256
|
if (note) {
|
|
232
257
|
// the last run here may be a teammate's: its ask is their words, so it is fenced like any memory
|
|
233
258
|
out.hookSpecificOutput = { hookEventName: 'SessionStart', additionalContext: fence(note) };
|
|
234
259
|
injected('last_run', note, true);
|
|
235
260
|
}
|
|
236
261
|
// say what leaves the laptop, every session (events.ts keeps it to that)
|
|
237
|
-
|
|
262
|
+
const ws = loadConfig(raw.cwd).workspace;
|
|
263
|
+
say(`${MARK} · on: team memory${ws ? ` in "${ws}"` : ''} · sends prompts, file paths and commands (no file contents)${note ? ' · picked up where the last run here left off' : ''}`);
|
|
238
264
|
}
|
|
239
265
|
const reply = toAgent(agent, name, out, session);
|
|
240
266
|
if (Object.keys(reply).length)
|
package/dist/index.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import {
|
|
5
|
-
import { daemon } from './daemon.js';
|
|
4
|
+
import { daemon, flush, unsent } from './daemon.js';
|
|
6
5
|
import { doctor, print } from './doctor.js';
|
|
7
6
|
import { approveChecks, checksHash, isApproved, loadChecks, readApprovals } from './done.js';
|
|
8
|
-
import { syncTree } from './tree.js';
|
|
7
|
+
import { codeSetup, repoKey, syncTree, writeSetup } from './tree.js';
|
|
8
|
+
import { gitRoot, projectOf } from './git.js';
|
|
9
9
|
import { api, ARCROOM_HOME, loadConfig, setOff } from './home.js';
|
|
10
10
|
import { hook } from './hook.js';
|
|
11
|
-
import { init } from './init.js';
|
|
11
|
+
import { init, uninstall } from './init.js';
|
|
12
|
+
import { sentReport } from './sent.js';
|
|
12
13
|
import { mcp } from './mcp.js';
|
|
13
|
-
import { readStats, statusText } from './signs.js';
|
|
14
|
+
import { readStats, statusText, wrongNote } from './signs.js';
|
|
14
15
|
import { sessionUsage, tokensOf } from './usage.js';
|
|
15
16
|
const [cmd, ...args] = process.argv.slice(2);
|
|
16
17
|
// A command someone typed that fails prints one line, not a stack trace (the hook, daemon and MCP server keep their own handling).
|
|
@@ -32,7 +33,7 @@ else if (cmd === 'doctor') {
|
|
|
32
33
|
process.exitCode = 1;
|
|
33
34
|
}
|
|
34
35
|
else if (cmd === 'whoami') {
|
|
35
|
-
const me = await api(loadConfig(), '/v1/me');
|
|
36
|
+
const me = await api(loadConfig(process.cwd()), '/v1/me');
|
|
36
37
|
console.log(`${me.email ?? 'shared key (no person)'} in workspace "${me.workspace}" (key ${me.key_label ?? 'unlabelled'}, made ${me.key_created_at})`);
|
|
37
38
|
}
|
|
38
39
|
else if (cmd === 'status') {
|
|
@@ -55,31 +56,29 @@ else if (cmd === 'rule') {
|
|
|
55
56
|
// arcroom rule [--tag <tag>] [--max-hits N] [--idle-days D] (no limits = remove the rule)
|
|
56
57
|
const opt = (name) => (args.includes(name) ? args[args.indexOf(name) + 1] : undefined);
|
|
57
58
|
const num = (name) => (opt(name) === undefined ? null : Number(opt(name)));
|
|
58
|
-
const res = await api(loadConfig(), '/v1/rules', { tag: opt('--tag') ?? null, max_hits: num('--max-hits'), idle_days: num('--idle-days') });
|
|
59
|
+
const res = await api(loadConfig(process.cwd()), '/v1/rules', { tag: opt('--tag') ?? null, max_hits: num('--max-hits'), idle_days: num('--idle-days') });
|
|
59
60
|
console.log(res.rule ? `rule set: ${JSON.stringify(res.rule)}` : 'rule removed');
|
|
60
61
|
}
|
|
61
62
|
else if (cmd === 'wrong' || cmd === 'share') {
|
|
62
63
|
// arcroom wrong [memo id] [reason]: the memory just handed over (or the one named) was wrong
|
|
63
|
-
// arcroom share
|
|
64
|
+
// arcroom share <memo id>: share an answer saved as "only me" with the team (recorded steps are the team's already)
|
|
64
65
|
const lastFile = join(ARCROOM_HOME, 'last-recall.json');
|
|
65
66
|
const named = /^[0-9a-f-]{36}$/i.test(args[0] ?? '') ? args.shift() : undefined;
|
|
66
|
-
const last = named ? { memo_id: named, intent: named } : existsSync(lastFile) ? JSON.parse(readFileSync(lastFile, 'utf8')) : undefined;
|
|
67
|
-
if (!last)
|
|
67
|
+
const last = named ? { memo_id: named, intent: named } : cmd === 'wrong' && existsSync(lastFile) ? JSON.parse(readFileSync(lastFile, 'utf8')) : undefined;
|
|
68
|
+
if (!last && cmd === 'share')
|
|
69
|
+
console.log('steps your agents record are shared with the team already; to share an answer saved as private, pass its id: arcroom share <memo id> (see the Memory page)');
|
|
70
|
+
else if (!last)
|
|
68
71
|
console.log('no memory was handed over yet; pass its id (see the Memory page)');
|
|
72
|
+
else if (!named && Array.isArray(last.handed) && last.handed.length > 1)
|
|
73
|
+
// several memories came in that hand-over: say which, instead of guessing
|
|
74
|
+
console.log(`The last hand-over had ${last.handed.length} memories. Name the wrong one:\n${last.handed.map((h) => ` arcroom wrong ${h.memo_id} ${h.kind}: ${h.text}`).join('\n')}`);
|
|
69
75
|
else if (cmd === 'share') {
|
|
70
|
-
await api(loadConfig(), '/v1/memory/share', { memo_id: last.memo_id });
|
|
76
|
+
await api(loadConfig(process.cwd()), '/v1/memory/share', { memo_id: last.memo_id });
|
|
71
77
|
console.log(`shared with the team: ${last.intent}`);
|
|
72
78
|
}
|
|
73
79
|
else {
|
|
74
|
-
const r = await api(loadConfig(), '/v1/memory/wrong', { memo_id: last.memo_id, reason: args.join(' ') || null });
|
|
75
|
-
|
|
76
|
-
? 'deleted (2 people said wrong)'
|
|
77
|
-
: r.steps_broken
|
|
78
|
-
? 'steps broken, the last good version takes over'
|
|
79
|
-
: r.answer_thrown_away
|
|
80
|
-
? 'answer thrown away'
|
|
81
|
-
: `noted (${r.reports} of 2 reports delete it)`;
|
|
82
|
-
console.log(`reported wrong: ${last.intent} · ${what}`);
|
|
80
|
+
const r = await api(loadConfig(process.cwd()), '/v1/memory/wrong', { memo_id: last.memo_id, reason: args.join(' ') || null });
|
|
81
|
+
console.log(`reported wrong: ${last.intent} · ${wrongNote(r)}`);
|
|
83
82
|
}
|
|
84
83
|
}
|
|
85
84
|
else if (cmd === 'code') {
|
|
@@ -90,23 +89,40 @@ else if (cmd === 'code') {
|
|
|
90
89
|
console.log('not inside a git repo');
|
|
91
90
|
else if (sub === 'off' || sub === 'on') {
|
|
92
91
|
writeSetup(s.root, { code: sub === 'off' ? 'off' : undefined });
|
|
93
|
-
forgetBlind(s.root); // the person here chose it: this laptop stops holding the repo to Blind
|
|
94
92
|
console.log(`Code wing ${sub} for ${s.root} (commit .arcroom.json so the team gets the same setting)`);
|
|
95
93
|
}
|
|
96
|
-
else if (s.mode
|
|
97
|
-
console.log(
|
|
94
|
+
else if (s.mode === 'off')
|
|
95
|
+
console.log('Code wing is off for this repo: arcroom code on turns it on');
|
|
98
96
|
else if (sub === 'sync') {
|
|
99
|
-
const r = await syncTree(loadConfig(), s);
|
|
97
|
+
const r = await syncTree(loadConfig(process.cwd()), s);
|
|
100
98
|
console.log(`sent ${r.nodes} folders and files (repo ${repoKey(s)})`);
|
|
101
99
|
}
|
|
102
100
|
else {
|
|
103
|
-
const repos = await api(loadConfig(), '/v1/code/tree');
|
|
101
|
+
const repos = await api(loadConfig(process.cwd()), '/v1/code/tree');
|
|
104
102
|
const me = repos.find((x) => x.repo === repoKey(s));
|
|
105
103
|
console.log(me
|
|
106
104
|
? `${me.repo}: ${me.folders} folders, ${me.files} files, ${me.editing} agent(s) editing now, default branch ${me.default_branch}`
|
|
107
105
|
: 'this repo has not sent its tree yet: arcroom code sync');
|
|
108
106
|
}
|
|
109
107
|
}
|
|
108
|
+
else if (cmd === 'flush') {
|
|
109
|
+
// arcroom flush: the last step of a CI job, so a headless run's final events are not lost when the machine goes away
|
|
110
|
+
const ok = await flush(process.argv[1]);
|
|
111
|
+
console.log(ok ? 'everything queued was sent' : `${unsent()} bytes still queued after 60 s: see arcroom doctor`);
|
|
112
|
+
if (!ok)
|
|
113
|
+
process.exitCode = 1;
|
|
114
|
+
}
|
|
115
|
+
else if (cmd === 'sent') {
|
|
116
|
+
// arcroom sent [YYYY-MM-DD] [--all]: what this laptop sent that day, kept 7 days
|
|
117
|
+
console.log(sentReport(args));
|
|
118
|
+
}
|
|
119
|
+
else if (cmd === 'uninstall') {
|
|
120
|
+
// arcroom uninstall [--all]: ArcRoom out of this project for every agent; --all also deletes ~/.arcroom
|
|
121
|
+
const changed = uninstall(process.cwd(), args.includes('--all'));
|
|
122
|
+
console.log(changed.length ? `Removed ArcRoom from:\n${changed.map((c) => ` ${c}`).join('\n')}` : 'Nothing of ArcRoom was set up in this project.');
|
|
123
|
+
if (!args.includes('--all'))
|
|
124
|
+
console.log('Other projects on this laptop keep it. arcroom uninstall --all also deletes ~/.arcroom (key, queue, runtime).');
|
|
125
|
+
}
|
|
110
126
|
else if (cmd === 'off' || cmd === 'on') {
|
|
111
127
|
// arcroom off | on: ArcRoom for the repo you are in, on this laptop only (nothing traced or sent while off)
|
|
112
128
|
const root = gitRoot(process.cwd()) ?? process.cwd();
|
|
@@ -123,7 +139,7 @@ else if (cmd === 'checks') {
|
|
|
123
139
|
console.log('no doneChecks in .arcroom.json here');
|
|
124
140
|
else if (args[0] === 'approve' && !process.stdin.isTTY)
|
|
125
141
|
// an agent's shell has no terminal: approving stays a step the person takes themselves (N14)
|
|
126
|
-
console.log(
|
|
142
|
+
console.log(`not approved: run arcroom checks approve yourself, in a terminal (not through an agent); in CI, set ARCROOM_APPROVE_CHECKS=${code} in the job's environment`);
|
|
127
143
|
else if (args[0] === 'approve') {
|
|
128
144
|
const done = approveChecks(process.cwd(), args[1] ?? '');
|
|
129
145
|
console.log(done ? `approved for ${process.cwd()} (after editing files, the agent is asked to run them with its Bash tool before it stops):\n${list}` : 'not approved: run arcroom checks, read the commands, then arcroom checks approve <the code it prints>');
|
|
@@ -143,9 +159,10 @@ else if (cmd === 'decide') {
|
|
|
143
159
|
else {
|
|
144
160
|
const over = opt('--over')?.split(',').map((o) => o.trim()).filter(Boolean);
|
|
145
161
|
const url = opt('--source');
|
|
146
|
-
const r = await api(loadConfig(), '/v1/memory/remember', {
|
|
162
|
+
const r = await api(loadConfig(process.cwd()), '/v1/memory/remember', {
|
|
147
163
|
kind: 'decision',
|
|
148
164
|
text,
|
|
165
|
+
project: projectOf(process.cwd()),
|
|
149
166
|
why: opt('--why') ?? null,
|
|
150
167
|
...(over ? { options: [text, ...over] } : {}),
|
|
151
168
|
...(opt('--context') ? { context: opt('--context') } : {}),
|
|
@@ -153,8 +170,12 @@ else if (cmd === 'decide') {
|
|
|
153
170
|
...(url ? { source: { kind: /\/pull\//.test(url) ? 'pr' : /\/issues\//.test(url) ? 'issue' : 'other', url } } : {}),
|
|
154
171
|
...(opt('--replaces') ? { replaces: opt('--replaces') } : {}),
|
|
155
172
|
});
|
|
156
|
-
|
|
173
|
+
// refused (replaced or reported wrong before): the API's note says which memory is in force and what to pass
|
|
174
|
+
if (!r.saved && r.note)
|
|
175
|
+
console.log(`decision not saved: ${r.note}`);
|
|
176
|
+
else
|
|
177
|
+
console.log(`${r.saved ? 'decision saved' : 'decision already saved; detail updated'}${r.replaced ? ', the earlier one is replaced' : ''} (${r.memo_id})`);
|
|
157
178
|
}
|
|
158
179
|
}
|
|
159
180
|
else
|
|
160
|
-
console.log('usage: arcroom init [--project <id>] (asks for the key; or set ARCROOM_API_KEY) [--url <arcroom api url>] | arcroom whoami | arcroom doctor | arcroom rule [--tag t] [--max-hits n] [--idle-days d] | arcroom wrong [memo id] [reason] | arcroom share [memo id] | arcroom decide "<choice>" [--why ...] [--over "A, B"] | arcroom code [status|sync|on|off] | arcroom checks [approve] | arcroom off | arcroom on');
|
|
181
|
+
console.log('usage: arcroom init [--project <id>] (asks for the key; or set ARCROOM_API_KEY) [--url <arcroom api url>] | arcroom whoami | arcroom doctor | arcroom rule [--tag t] [--max-hits n] [--idle-days d] | arcroom wrong [memo id] [reason] | arcroom share [memo id] | arcroom decide "<choice>" [--why ...] [--over "A, B"] | arcroom code [status|sync|on|off] | arcroom checks [approve] | arcroom off | arcroom on | arcroom uninstall [--all] | arcroom init --everywhere | arcroom sent [YYYY-MM-DD] [--all] | arcroom flush');
|
package/dist/init.js
CHANGED
|
@@ -1,26 +1,30 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
|
-
import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { dirname, join } from 'node:path';
|
|
2
|
+
import { appendFileSync, cpSync, existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, isAbsolute, join, relative } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { createInterface } from 'node:readline/promises';
|
|
6
6
|
import { AGENTS, CURSOR_HOOKS, opencodePlugin } from './agents.js';
|
|
7
7
|
import { stopDaemon } from './daemon.js';
|
|
8
|
-
import { onPath } from './git.js';
|
|
9
|
-
import { apiUrl, ARCROOM_HOME, DEFAULT_URL, isLocalUrl, keyRefused } from './home.js';
|
|
8
|
+
import { git, onPath, projectOf } from './git.js';
|
|
9
|
+
import { api, apiUrl, ARCROOM_HOME, DEFAULT_URL, isLocalUrl, keyRefused } from './home.js';
|
|
10
|
+
import { userSettings } from './scope.js';
|
|
10
11
|
const EVENTS = ['SessionStart', 'UserPromptSubmit', 'PreToolUse', 'PostToolUse', 'Stop', 'SessionEnd'];
|
|
11
12
|
const TOOL_EVENTS = new Set(['PreToolUse', 'PostToolUse']);
|
|
12
13
|
const MARK = '.arcroom/runtime'; // identifies our hook entries in a settings file
|
|
13
14
|
// node by its full path, never the word "node": an agent starting a hook inside a repo must not run a node.exe planted there
|
|
14
15
|
const NODE = process.execPath;
|
|
15
16
|
const nodeCmd = JSON.stringify(NODE);
|
|
17
|
+
// After a tool, only a shell run (a done-check passing) and ArcRoom's own tools matter, so Claude Code launches the hook
|
|
18
|
+
// once per tool call instead of twice: each launch is a window flash on Windows (Git Bash, no console of its own).
|
|
19
|
+
export const AFTER_TOOLS = 'Bash|PowerShell|mcp__arcroom__.*';
|
|
16
20
|
/** Puts ArcRoom's hook on every event: replaces an earlier ArcRoom entry, keeps everyone else's hooks. */
|
|
17
|
-
export function withArcroomHooks(settings, command, anyTool = '*') {
|
|
21
|
+
export function withArcroomHooks(settings, command, anyTool = '*', afterTool = anyTool) {
|
|
18
22
|
const hooks = { ...settings.hooks };
|
|
19
23
|
for (const event of EVENTS) {
|
|
20
24
|
const others = (hooks[event] ?? []).filter((g) => !g.hooks.some((h) => h.command.includes(MARK)));
|
|
21
25
|
const ours = { hooks: [{ type: 'command', command }] };
|
|
22
26
|
if (TOOL_EVENTS.has(event))
|
|
23
|
-
ours.matcher = anyTool;
|
|
27
|
+
ours.matcher = event === 'PostToolUse' ? afterTool : anyTool;
|
|
24
28
|
hooks[event] = [...others, ours];
|
|
25
29
|
}
|
|
26
30
|
return { ...settings, hooks };
|
|
@@ -38,6 +42,28 @@ export function withCodexMcp(toml, entry, node = NODE) {
|
|
|
38
42
|
const ours = `[mcp_servers.arcroom]\ncommand = ${JSON.stringify(node)}\nargs = [${JSON.stringify(entry)}, "mcp"]\n`;
|
|
39
43
|
return (others ? `${others}\n\n` : '') + ours;
|
|
40
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Keeps the files init wrote out of commits, in this clone only (.git/info/exclude, never committed): they hold this
|
|
47
|
+
* laptop's paths, so a `git add -A` would break a teammate's setup. A file the repo already tracks is left alone.
|
|
48
|
+
*/
|
|
49
|
+
export function keepOutOfGit(files, cwd) {
|
|
50
|
+
const root = git(['rev-parse', '--show-toplevel'], { cwd }).stdout?.trim();
|
|
51
|
+
const path = git(['rev-parse', '--git-path', 'info/exclude'], { cwd }).stdout?.trim();
|
|
52
|
+
if (!root || !path)
|
|
53
|
+
return [];
|
|
54
|
+
const exclude = isAbsolute(path) ? path : join(cwd, path);
|
|
55
|
+
const have = existsSync(exclude) ? readFileSync(exclude, 'utf8').split(/\r?\n/) : [];
|
|
56
|
+
// git answers with the real path; a symlinked folder (macOS /var) or a short name (Windows RUNNER~1) would read as outside
|
|
57
|
+
const real = (p) => (existsSync(p) ? realpathSync.native(p) : p);
|
|
58
|
+
const add = files
|
|
59
|
+
.map((f) => '/' + relative(real(root), real(f)).replaceAll('\\', '/'))
|
|
60
|
+
.filter((rel) => !rel.startsWith('/..') && !have.includes(rel) && git(['ls-files', '--error-unmatch', rel.slice(1)], { cwd: root }).status !== 0);
|
|
61
|
+
if (!add.length)
|
|
62
|
+
return [];
|
|
63
|
+
mkdirSync(dirname(exclude), { recursive: true });
|
|
64
|
+
appendFileSync(exclude, (have.length && have[have.length - 1] !== '' ? '\n' : '') + add.join('\n') + '\n');
|
|
65
|
+
return add;
|
|
66
|
+
}
|
|
41
67
|
const readJson = (file) => (existsSync(file) ? JSON.parse(readFileSync(file, 'utf8')) : {});
|
|
42
68
|
const writeJson = (file, value) => {
|
|
43
69
|
mkdirSync(dirname(file), { recursive: true });
|
|
@@ -58,6 +84,26 @@ export function initChoice(o) {
|
|
|
58
84
|
];
|
|
59
85
|
return { key, url, notes };
|
|
60
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* The config after init saves `key` in `repo`. A key for another workspace than the laptop's working default becomes this
|
|
89
|
+
* repo's own, and every other repo keeps sending where it did (a freelancer's two clients); otherwise it is the default.
|
|
90
|
+
*/
|
|
91
|
+
export function keysAfterInit(saved, repo, key, url, workspace) {
|
|
92
|
+
const other = !!saved.apiKey && saved.apiKey !== key && !keyRefused({ apiKey: saved.apiKey, url }) && !!saved.workspace && !!workspace && saved.workspace !== workspace;
|
|
93
|
+
const repos = { ...(saved.repos ?? {}) };
|
|
94
|
+
if (other)
|
|
95
|
+
repos[repo] = { apiKey: key, workspace };
|
|
96
|
+
else
|
|
97
|
+
delete repos[repo];
|
|
98
|
+
const next = other ? { ...saved, url, repos } : { ...saved, apiKey: key, url, ...(workspace ? { workspace } : {}), repos };
|
|
99
|
+
if (!Object.keys(next.repos).length)
|
|
100
|
+
delete next.repos;
|
|
101
|
+
if (other)
|
|
102
|
+
console.log(`This repo sends to "${workspace}"; other repos on this laptop keep sending to "${saved.workspace}".`);
|
|
103
|
+
else if (saved.workspace && workspace && saved.workspace !== workspace)
|
|
104
|
+
console.error(`Note: this laptop's default is now "${workspace}" (was "${saved.workspace}").`);
|
|
105
|
+
return next;
|
|
106
|
+
}
|
|
61
107
|
export async function init(args) {
|
|
62
108
|
const arg = (name) => (args.includes(name) ? args[args.indexOf(name) + 1] : undefined);
|
|
63
109
|
const agent = (arg('--agent') ?? 'claude');
|
|
@@ -67,7 +113,10 @@ export async function init(args) {
|
|
|
67
113
|
}
|
|
68
114
|
const configFile = join(ARCROOM_HOME, 'config.json');
|
|
69
115
|
const saved = existsSync(configFile) ? JSON.parse(readFileSync(configFile, 'utf8')) : {}; // re-running init reuses saved values
|
|
70
|
-
const
|
|
116
|
+
const repo = projectOf(process.cwd());
|
|
117
|
+
const mine = saved.repos?.[repo]; // this repo's own key, when it has one
|
|
118
|
+
const forRepo = mine ? { ...saved, apiKey: mine.apiKey, workspace: mine.workspace } : saved;
|
|
119
|
+
const choice = initChoice({ argKey: arg('--key'), envKey: process.env.ARCROOM_API_KEY, argUrl: arg('--url'), saved: forRepo });
|
|
71
120
|
let key = choice.key;
|
|
72
121
|
const url = choice.url;
|
|
73
122
|
if (arg('--key'))
|
|
@@ -90,9 +139,24 @@ export async function init(args) {
|
|
|
90
139
|
console.error('That is not an ArcRoom key (it starts with ark_).');
|
|
91
140
|
process.exit(1);
|
|
92
141
|
}
|
|
93
|
-
// 1.
|
|
142
|
+
// 1. Check the key with the API, then remember it for this machine: a typo or a revoked key must not end in "ArcRoom is on".
|
|
143
|
+
let workspace = forRepo.apiKey === key ? forRepo.workspace : undefined;
|
|
144
|
+
try {
|
|
145
|
+
const me = await api({ apiKey: key, url }, '/v1/me');
|
|
146
|
+
workspace = me.workspace;
|
|
147
|
+
console.log(`Connected to workspace "${me.workspace}"${me.email ? ` as ${me.email}` : ''}.`);
|
|
148
|
+
}
|
|
149
|
+
catch (e) {
|
|
150
|
+
if (/invalid api key/.test(e.message)) {
|
|
151
|
+
console.error('The API refused this key. Make a new one in the web app (Settings → API keys) and run arcroom init again.');
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
console.error(`Could not check the key (${e.message}); saved it anyway. Run arcroom doctor once you are online.`);
|
|
155
|
+
}
|
|
94
156
|
mkdirSync(ARCROOM_HOME, { recursive: true, mode: 0o700 });
|
|
95
|
-
|
|
157
|
+
const cfg = keysAfterInit(saved, repo, key, url, workspace);
|
|
158
|
+
cfg.setUp = [...new Set([...(cfg.setUp ?? []), repo])]; // laptop-wide hooks (--everywhere) act only in repos set up here
|
|
159
|
+
writeFileSync(configFile, JSON.stringify(cfg, null, 2), { mode: 0o600 });
|
|
96
160
|
stopDaemon(); // the next hook event starts the updated one
|
|
97
161
|
// 2. Copy the hook runtime to a stable place (npx's own copy is temporary).
|
|
98
162
|
const runtime = join(ARCROOM_HOME, 'runtime');
|
|
@@ -113,7 +177,7 @@ export async function init(args) {
|
|
|
113
177
|
}
|
|
114
178
|
if (agent !== 'claude')
|
|
115
179
|
return initOther(agent, entry, project);
|
|
116
|
-
const settings = withArcroomHooks(current, `${nodeCmd} "${entry}" hook${project ? ` --project ${project}` : ''}
|
|
180
|
+
const settings = withArcroomHooks(current, `${nodeCmd} "${entry}" hook${project ? ` --project ${project}` : ''}`, '*', AFTER_TOOLS);
|
|
117
181
|
// never pre-approve a server from the shared .mcp.json by name: a pull request could change what that name runs
|
|
118
182
|
const approved = (current.enabledMcpjsonServers ?? []).filter((n) => n !== 'arcroom');
|
|
119
183
|
if (approved.length)
|
|
@@ -121,9 +185,13 @@ export async function init(args) {
|
|
|
121
185
|
else
|
|
122
186
|
delete settings.enabledMcpjsonServers;
|
|
123
187
|
// Live badge at the bottom of Claude Code ("ArcRoom · live · 2 reused"), unless the project has its own status line.
|
|
188
|
+
// It runs a command on every redraw, a window flash each time on Windows, so there it is only on with --status.
|
|
124
189
|
const theirs = current.statusLine;
|
|
125
|
-
|
|
190
|
+
const status = process.platform !== 'win32' || args.includes('--status');
|
|
191
|
+
if (status && (!theirs || theirs.command?.includes(MARK)))
|
|
126
192
|
settings.statusLine = { type: 'command', command: `${nodeCmd} "${entry}" status`, padding: 0 };
|
|
193
|
+
else if (!status && theirs?.command?.includes(MARK))
|
|
194
|
+
delete settings.statusLine;
|
|
127
195
|
mkdirSync(dirname(file), { recursive: true });
|
|
128
196
|
writeFileSync(file, JSON.stringify(settings, null, 2) + '\n');
|
|
129
197
|
// 4. Register the MCP server (find_task / claim_task / complete_task) for this project: in Claude Code's local scope
|
|
@@ -142,17 +210,28 @@ export async function init(args) {
|
|
|
142
210
|
mcpConfig.mcpServers = { ...mcpConfig.mcpServers, arcroom: { command: NODE, args: [entry, 'mcp'] } };
|
|
143
211
|
writeFileSync(mcpFile, JSON.stringify(mcpConfig, null, 2) + '\n');
|
|
144
212
|
}
|
|
213
|
+
const kept = keepOutOfGit([file, ...(local ? [] : [mcpFile])], process.cwd());
|
|
214
|
+
// 5. --everywhere: the same hook and tools for every folder on this laptop (Claude Code's user settings and user scope),
|
|
215
|
+
// acting only in repos set up here, so other clones and worktrees of this repo are covered too.
|
|
216
|
+
const everywhere = args.includes('--everywhere');
|
|
217
|
+
if (everywhere) {
|
|
218
|
+
const user = userSettings();
|
|
219
|
+
const mine = existsSync(user) ? JSON.parse(readFileSync(user, 'utf8')) : {};
|
|
220
|
+
writeJson(user, withArcroomHooks(mine, `${nodeCmd} "${entry}" hook --user`, '*', AFTER_TOOLS));
|
|
221
|
+
claudeCli(['mcp', 'remove', '--scope', 'user', 'arcroom']);
|
|
222
|
+
claudeCli(['mcp', 'add', '--scope', 'user', 'arcroom', '--', NODE, entry, 'mcp', '--user']);
|
|
223
|
+
}
|
|
145
224
|
console.log(`ArcRoom is on for this project.
|
|
146
225
|
key ${join(ARCROOM_HOME, 'config.json')}
|
|
147
226
|
hooks ${file}
|
|
148
|
-
tools ${local ? 'Claude Code local scope (claude mcp list)' : `${mcpFile} (approve "arcroom" when Claude Code asks)`}
|
|
149
|
-
Start a new Claude Code session here to begin capturing runs
|
|
227
|
+
tools ${local ? 'Claude Code local scope (claude mcp list)' : `${mcpFile} (approve "arcroom" when Claude Code asks)`}${kept.length ? `\n git kept out of commits in this clone: ${kept.join(', ')}` : ''}${everywhere ? `\n laptop ${userSettings()}: every clone and worktree of a repo set up here` : ''}
|
|
228
|
+
Start a new Claude Code session here to begin capturing runs. Check it any time: arcroom doctor`);
|
|
150
229
|
}
|
|
151
|
-
/**
|
|
152
|
-
function
|
|
230
|
+
/** Runs the claude command; null when it is not on PATH. */
|
|
231
|
+
function claudeCli(args) {
|
|
153
232
|
const claude = onPath(process.platform === 'win32' ? ['claude.exe', 'claude.cmd'] : ['claude']);
|
|
154
233
|
if (!claude)
|
|
155
|
-
return
|
|
234
|
+
return null;
|
|
156
235
|
const run = (args) => claude.endsWith('.cmd') // an npm install on Windows: a batch file runs through cmd, every argument quoted
|
|
157
236
|
? spawnSync(process.env.ComSpec ?? 'cmd.exe', ['/d', '/s', '/c', `"${[claude, ...args].map((a) => `"${a}"`).join(' ')}"`], {
|
|
158
237
|
windowsVerbatimArguments: true,
|
|
@@ -160,8 +239,106 @@ function addLocalMcp(entry) {
|
|
|
160
239
|
encoding: 'utf8',
|
|
161
240
|
})
|
|
162
241
|
: spawnSync(claude, args, { encoding: 'utf8', windowsHide: true });
|
|
163
|
-
run(
|
|
164
|
-
|
|
242
|
+
return run(args).status;
|
|
243
|
+
}
|
|
244
|
+
/** `claude mcp add --scope local arcroom`: false when the claude command is not on PATH or the add failed. */
|
|
245
|
+
function addLocalMcp(entry) {
|
|
246
|
+
if (claudeCli(['mcp', 'remove', '--scope', 'local', 'arcroom']) === null)
|
|
247
|
+
return false; // none yet is fine
|
|
248
|
+
return claudeCli(['mcp', 'add', '--scope', 'local', 'arcroom', '--', NODE, entry, 'mcp']) === 0;
|
|
249
|
+
}
|
|
250
|
+
/** A hooks map without ArcRoom's entries (Claude Code and Codex group them; Cursor lists commands); empty events go. */
|
|
251
|
+
function withoutOurHooks(hooks) {
|
|
252
|
+
if (!hooks)
|
|
253
|
+
return hooks;
|
|
254
|
+
const ours = (h) => String(h?.command ?? '').includes(MARK) || (h?.hooks ?? []).some((x) => String(x?.command ?? '').includes(MARK));
|
|
255
|
+
const next = Object.fromEntries(Object.entries(hooks).map(([event, list]) => [event, (list ?? []).filter((h) => !ours(h))]).filter(([, list]) => list.length));
|
|
256
|
+
return Object.keys(next).length ? next : undefined;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* `arcroom uninstall [--all]`: takes out of this project what init put in for every agent (hooks, status line, MCP entries,
|
|
260
|
+
* the OpenCode plugin), keeping everyone else's settings, and stops the uploader. --all also removes ~/.arcroom (the key,
|
|
261
|
+
* the queue, the runtime). Returns what it changed.
|
|
262
|
+
*/
|
|
263
|
+
export function uninstall(cwd, all = false, removeLocalMcp = () => claudeCli(['mcp', 'remove', '--scope', 'local', 'arcroom']), removeUserMcp = () => claudeCli(['mcp', 'remove', '--scope', 'user', 'arcroom'])) {
|
|
264
|
+
const changed = [];
|
|
265
|
+
const edit = (file, change) => {
|
|
266
|
+
if (!existsSync(file))
|
|
267
|
+
return;
|
|
268
|
+
const before = JSON.parse(readFileSync(file, 'utf8'));
|
|
269
|
+
const after = change(structuredClone(before));
|
|
270
|
+
if (JSON.stringify(after) === JSON.stringify(before))
|
|
271
|
+
return;
|
|
272
|
+
if (!Object.keys(after).length)
|
|
273
|
+
rmSync(file);
|
|
274
|
+
else
|
|
275
|
+
writeFileSync(file, JSON.stringify(after, null, 2) + '\n');
|
|
276
|
+
changed.push(file);
|
|
277
|
+
};
|
|
278
|
+
const ourServer = (s) => JSON.stringify(s ?? '').replaceAll('\\\\', '/').includes(MARK);
|
|
279
|
+
const hooksOf = (v) => {
|
|
280
|
+
const hooks = withoutOurHooks(v.hooks);
|
|
281
|
+
if (hooks)
|
|
282
|
+
v.hooks = hooks;
|
|
283
|
+
else
|
|
284
|
+
delete v.hooks;
|
|
285
|
+
if (String(v.statusLine?.command ?? '').includes(MARK))
|
|
286
|
+
delete v.statusLine;
|
|
287
|
+
return v;
|
|
288
|
+
};
|
|
289
|
+
edit(join(cwd, '.claude', 'settings.local.json'), hooksOf);
|
|
290
|
+
edit(join(cwd, '.codex', 'hooks.json'), hooksOf);
|
|
291
|
+
edit(join(cwd, '.cursor', 'hooks.json'), (v) => {
|
|
292
|
+
const hooks = withoutOurHooks(v.hooks);
|
|
293
|
+
if (hooks)
|
|
294
|
+
v.hooks = hooks;
|
|
295
|
+
else
|
|
296
|
+
delete v.hooks;
|
|
297
|
+
return Object.keys(v).length === 1 && v.version !== undefined ? {} : v;
|
|
298
|
+
});
|
|
299
|
+
for (const file of [join(cwd, '.mcp.json'), join(cwd, '.cursor', 'mcp.json')])
|
|
300
|
+
edit(file, (v) => {
|
|
301
|
+
if (ourServer(v.mcpServers?.arcroom))
|
|
302
|
+
delete v.mcpServers.arcroom;
|
|
303
|
+
if (v.mcpServers && !Object.keys(v.mcpServers).length)
|
|
304
|
+
delete v.mcpServers;
|
|
305
|
+
return v;
|
|
306
|
+
});
|
|
307
|
+
edit(join(cwd, 'opencode.json'), (v) => {
|
|
308
|
+
if (ourServer(v.mcp?.arcroom))
|
|
309
|
+
delete v.mcp.arcroom;
|
|
310
|
+
if (v.mcp && !Object.keys(v.mcp).length)
|
|
311
|
+
delete v.mcp;
|
|
312
|
+
return Object.keys(v).length === 1 && v.$schema ? {} : v;
|
|
313
|
+
});
|
|
314
|
+
const toml = join(cwd, '.codex', 'config.toml');
|
|
315
|
+
if (existsSync(toml)) {
|
|
316
|
+
const before = readFileSync(toml, 'utf8');
|
|
317
|
+
const after = before.replace(/\n?\[mcp_servers\.arcroom\][\s\S]*?(?=\n\[|$)/, '').trimEnd();
|
|
318
|
+
if (after !== before.trimEnd()) {
|
|
319
|
+
if (after)
|
|
320
|
+
writeFileSync(toml, after + '\n');
|
|
321
|
+
else
|
|
322
|
+
rmSync(toml);
|
|
323
|
+
changed.push(toml);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const plugin = join(cwd, '.opencode', 'plugins', 'arcroom.js');
|
|
327
|
+
if (existsSync(plugin))
|
|
328
|
+
rmSync(plugin), changed.push(plugin);
|
|
329
|
+
if (removeLocalMcp() === 0)
|
|
330
|
+
changed.push('Claude Code local MCP entry "arcroom"');
|
|
331
|
+
if (all) {
|
|
332
|
+
edit(userSettings(), hooksOf);
|
|
333
|
+
if (removeUserMcp() === 0)
|
|
334
|
+
changed.push('Claude Code user MCP entry "arcroom"');
|
|
335
|
+
}
|
|
336
|
+
stopDaemon();
|
|
337
|
+
if (all && existsSync(ARCROOM_HOME)) {
|
|
338
|
+
rmSync(ARCROOM_HOME, { recursive: true, force: true });
|
|
339
|
+
changed.push(ARCROOM_HOME);
|
|
340
|
+
}
|
|
341
|
+
return changed;
|
|
165
342
|
}
|
|
166
343
|
/** Steps 3 and 4 for Codex, Cursor and OpenCode: their own hook and MCP files, in this project. */
|
|
167
344
|
function initOther(agent, entry, project) {
|
|
@@ -202,8 +379,10 @@ function initOther(agent, entry, project) {
|
|
|
202
379
|
cursor: 'Restart Cursor. Cursor adds context only after a tool call, so team memory reaches the agent on its first tool use.',
|
|
203
380
|
opencode: "Restart OpenCode. Experimental: per-prompt recall rides OpenCode's experimental system-prompt hook, so it may change with OpenCode.",
|
|
204
381
|
};
|
|
382
|
+
const kept = keepOutOfGit(written, cwd);
|
|
205
383
|
console.log(`ArcRoom is on for ${agent} in this project.
|
|
206
384
|
key ${join(ARCROOM_HOME, 'config.json')}
|
|
207
|
-
${written.map((f) => ` wrote ${f}`).join('\n')}
|
|
208
|
-
${next[agent]}
|
|
385
|
+
${written.map((f) => ` wrote ${f}`).join('\n')}${kept.length ? `\n git kept out of commits in this clone: ${kept.join(', ')}` : ''}
|
|
386
|
+
${next[agent]}
|
|
387
|
+
Check it any time: arcroom doctor`);
|
|
209
388
|
}
|
package/dist/lookup.js
CHANGED
|
@@ -36,7 +36,7 @@ const CODE_MS = 1200; // the code map is the extra: it never holds recall up
|
|
|
36
36
|
export async function lookup(prompt, cwd, session, given) {
|
|
37
37
|
const [r, code] = await Promise.all([
|
|
38
38
|
recall(prompt, cwd, projectOf(cwd), given, RECALL_MS).catch(() => undefined),
|
|
39
|
-
codeContext(loadConfig(), cwd, session, prompt, CODE_MS).catch(() => undefined),
|
|
39
|
+
codeContext(loadConfig(cwd), cwd, session, prompt, CODE_MS).catch(() => undefined),
|
|
40
40
|
]);
|
|
41
41
|
if (!r)
|
|
42
42
|
markDown();
|