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/usage.js
CHANGED
|
@@ -29,6 +29,20 @@ export function sumUsage(text, seen) {
|
|
|
29
29
|
catch {
|
|
30
30
|
continue;
|
|
31
31
|
}
|
|
32
|
+
// Codex's session file: the model in turn_context, and a token_count event after each model call. A repeated event
|
|
33
|
+
// carries the same running total, which is its id here. Its input includes the cached part; reasoning is in output.
|
|
34
|
+
if (e?.type === 'turn_context' && typeof e.payload?.model === 'string')
|
|
35
|
+
usage.model = e.payload.model;
|
|
36
|
+
const info = e?.type === 'event_msg' && e.payload?.type === 'token_count' ? e.payload.info : undefined;
|
|
37
|
+
const last = info?.last_token_usage;
|
|
38
|
+
const id = `codex:${info?.total_token_usage?.total_tokens}`;
|
|
39
|
+
if (last && !seen.has(id)) {
|
|
40
|
+
seen.add(id);
|
|
41
|
+
const cached = last.cached_input_tokens ?? 0;
|
|
42
|
+
usage.input += Math.max(0, (last.input_tokens ?? 0) - cached);
|
|
43
|
+
usage.cache_read += cached;
|
|
44
|
+
usage.output += last.output_tokens ?? 0;
|
|
45
|
+
}
|
|
32
46
|
const m = e?.type === 'assistant' ? e.message : undefined;
|
|
33
47
|
if (m?.usage && m.id && !seen.has(m.id))
|
|
34
48
|
latest.set(m.id, m);
|
package/package.json
CHANGED
package/dist/blind.js
DELETED
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
import { git as runGit } from './git.js';
|
|
2
|
-
import { createCipheriv, createDecipheriv, createHmac, hkdfSync, randomBytes } from 'node:crypto';
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
-
import { isAbsolute, join, relative } from 'node:path';
|
|
5
|
-
import { ARCROOM_HOME } from './home.js';
|
|
6
|
-
import { projectOf } from './session.js';
|
|
7
|
-
const mac = (key, s) => createHmac('sha256', key).update(s).digest('hex');
|
|
8
|
-
/** The three working keys and the fingerprint, all derived from the 32-byte team key. */
|
|
9
|
-
export function keysFrom(root) {
|
|
10
|
-
const sub = (info) => Buffer.from(hkdfSync('sha256', root, Buffer.alloc(0), info, 32));
|
|
11
|
-
return { root, fp: mac(root, 'arcroom fingerprint').slice(0, 16), path: sub('arcroom paths'), hash: sub('arcroom hashes'), enc: sub('arcroom notes') };
|
|
12
|
-
}
|
|
13
|
-
export const newKeys = () => keysFrom(randomBytes(32));
|
|
14
|
-
/** src/billing → k_7f3a…; the repo root stays "." (it says nothing). */
|
|
15
|
-
export const scramblePath = (k, rel) => (rel === '' || rel === '.' ? '.' : `k_${mac(k.path, rel).slice(0, 24)}`);
|
|
16
|
-
/** A git hash → c_…; the trailing * (uncommitted edits) is kept so "dirty" stays visible. */
|
|
17
|
-
export const scrambleHash = (k, h) => (h.endsWith('*') ? `c_${mac(k.hash, h.slice(0, -1)).slice(0, 24)}*` : `c_${mac(k.hash, h).slice(0, 24)}`);
|
|
18
|
-
/** A branch (b_) or repo (r_) name. */
|
|
19
|
-
export const scrambleName = (k, kind, name) => `${kind}_${mac(k.path, `${kind}:${name}`).slice(0, 20)}`;
|
|
20
|
-
export function encrypt(k, text) {
|
|
21
|
-
const iv = randomBytes(12);
|
|
22
|
-
const c = createCipheriv('aes-256-gcm', k.enc, iv);
|
|
23
|
-
const body = Buffer.concat([c.update(text, 'utf8'), c.final()]);
|
|
24
|
-
return `enc:v1:${Buffer.concat([iv, c.getAuthTag(), body]).toString('base64')}`;
|
|
25
|
-
}
|
|
26
|
-
/** The text, or undefined when it was sealed with another key. Plain text passes through. */
|
|
27
|
-
export function decrypt(k, sealed) {
|
|
28
|
-
if (!sealed.startsWith('enc:v1:'))
|
|
29
|
-
return sealed;
|
|
30
|
-
try {
|
|
31
|
-
const b = Buffer.from(sealed.slice(7), 'base64');
|
|
32
|
-
const d = createDecipheriv('aes-256-gcm', k.enc, b.subarray(0, 12));
|
|
33
|
-
d.setAuthTag(b.subarray(12, 28));
|
|
34
|
-
return Buffer.concat([d.update(b.subarray(28)), d.final()]).toString('utf8');
|
|
35
|
-
}
|
|
36
|
-
catch {
|
|
37
|
-
return undefined;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
/** The key as a code to pass to a teammate, or to keep as the recovery code. */
|
|
41
|
-
export const keyCode = (k) => `arck1_${k.root.toString('base64url')}`;
|
|
42
|
-
export function fromCode(code) {
|
|
43
|
-
const m = /^arck1_([A-Za-z0-9_-]{43})$/.exec(code.trim());
|
|
44
|
-
if (!m)
|
|
45
|
-
throw new Error('that is not an ArcRoom key code (it starts with arck1_)');
|
|
46
|
-
return keysFrom(Buffer.from(m[1], 'base64url'));
|
|
47
|
-
}
|
|
48
|
-
const keyFile = (fp) => join(ARCROOM_HOME, 'keys', `${fp}.key`);
|
|
49
|
-
export function saveKey(k) {
|
|
50
|
-
mkdirSync(join(ARCROOM_HOME, 'keys'), { recursive: true });
|
|
51
|
-
writeFileSync(keyFile(k.fp), keyCode(k), { mode: 0o600 });
|
|
52
|
-
return keyFile(k.fp);
|
|
53
|
-
}
|
|
54
|
-
export function loadKey(fp) {
|
|
55
|
-
try {
|
|
56
|
-
const k = fromCode(readFileSync(keyFile(fp), 'utf8'));
|
|
57
|
-
return k.fp === fp ? k : undefined;
|
|
58
|
-
}
|
|
59
|
-
catch {
|
|
60
|
-
return undefined;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
const git = (cwd, args) => {
|
|
64
|
-
const r = runGit(args, { cwd, timeout: 3000, maxBuffer: 64 * 1024 * 1024 });
|
|
65
|
-
return r.status === 0 ? r.stdout : undefined;
|
|
66
|
-
};
|
|
67
|
-
export const gitRoot = (cwd) => git(cwd, ['rev-parse', '--show-toplevel'])?.trim();
|
|
68
|
-
/** The repo's Code wing setup: its mode from .arcroom.json, and in Blind mode the key when this laptop has it. */
|
|
69
|
-
export function codeSetup(cwd) {
|
|
70
|
-
const root = cwd ? gitRoot(cwd) : undefined;
|
|
71
|
-
if (!root)
|
|
72
|
-
return undefined;
|
|
73
|
-
const cfg = readSetup(root);
|
|
74
|
-
const mode = cfg.code === 'blind' || cfg.code === 'off' ? cfg.code : 'standard';
|
|
75
|
-
// N12: a repo seen in Blind mode on this laptop stays Blind even if a commit takes it out of .arcroom.json;
|
|
76
|
-
// only the person here lowers it (arcroom code on|off)
|
|
77
|
-
const known = blindRepos()[root];
|
|
78
|
-
if (mode !== 'blind' && known)
|
|
79
|
-
return { root, mode: 'blind', fp: known, keys: loadKey(known) };
|
|
80
|
-
if (mode === 'blind' && typeof cfg.key === 'string' && known !== cfg.key)
|
|
81
|
-
rememberBlind(root, cfg.key);
|
|
82
|
-
return mode === 'blind' ? { root, mode, fp: cfg.key, keys: typeof cfg.key === 'string' ? loadKey(cfg.key) : undefined } : { root, mode };
|
|
83
|
-
}
|
|
84
|
-
/** Repos this laptop has seen in Blind mode: git root -> key fingerprint. */
|
|
85
|
-
const BLIND_REPOS = join(ARCROOM_HOME, 'blind-repos.json');
|
|
86
|
-
const blindRepos = () => {
|
|
87
|
-
try {
|
|
88
|
-
return JSON.parse(readFileSync(BLIND_REPOS, 'utf8'));
|
|
89
|
-
}
|
|
90
|
-
catch {
|
|
91
|
-
return {};
|
|
92
|
-
}
|
|
93
|
-
};
|
|
94
|
-
const saveBlindRepos = (repos) => {
|
|
95
|
-
mkdirSync(ARCROOM_HOME, { recursive: true, mode: 0o700 });
|
|
96
|
-
writeFileSync(BLIND_REPOS, JSON.stringify(repos, null, 2) + '\n', { mode: 0o600 });
|
|
97
|
-
};
|
|
98
|
-
const rememberBlind = (root, fp) => saveBlindRepos({ ...blindRepos(), [root]: fp });
|
|
99
|
-
/** The person chose another mode for this repo (arcroom code on|off): it is no longer kept Blind. */
|
|
100
|
-
export function forgetBlind(root) {
|
|
101
|
-
const { [root]: _gone, ...rest } = blindRepos();
|
|
102
|
-
saveBlindRepos(rest);
|
|
103
|
-
}
|
|
104
|
-
export function readSetup(root) {
|
|
105
|
-
try {
|
|
106
|
-
return JSON.parse(readFileSync(join(root, '.arcroom.json'), 'utf8'));
|
|
107
|
-
}
|
|
108
|
-
catch {
|
|
109
|
-
return {};
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
export function writeSetup(root, patch) {
|
|
113
|
-
const next = { ...readSetup(root), ...patch };
|
|
114
|
-
for (const [k, v] of Object.entries(next))
|
|
115
|
-
if (v === undefined)
|
|
116
|
-
delete next[k];
|
|
117
|
-
writeFileSync(join(root, '.arcroom.json'), JSON.stringify(next, null, 2) + '\n');
|
|
118
|
-
}
|
|
119
|
-
/** The repo id as the Room knows it: the git remote, scrambled in Blind mode. */
|
|
120
|
-
export const repoKey = (s) => (s.mode === 'blind' && s.keys ? scrambleName(s.keys, 'r', projectOf(s.root)) : projectOf(s.root));
|
|
121
|
-
export const branchKey = (s, branch) => (s.mode === 'blind' && s.keys ? scrambleName(s.keys, 'b', branch) : branch);
|
|
122
|
-
export const nodeKey = (s, rel) => (s.mode === 'blind' && s.keys ? scramblePath(s.keys, rel) : rel || '.');
|
|
123
|
-
export const hashKey = (s, h) => (s.mode === 'blind' && s.keys ? scrambleHash(s.keys, h) : h);
|
|
124
|
-
/** A path (absolute, or relative to `from`) as a repo path: "src/billing/tax.ts", or undefined outside the repo. */
|
|
125
|
-
export function repoPath(root, p, from = root) {
|
|
126
|
-
const rel = relative(root, isAbsolute(p) ? p : join(from, p)).replaceAll('\\', '/');
|
|
127
|
-
return rel.startsWith('..') || isAbsolute(rel) ? undefined : rel || '.';
|
|
128
|
-
}
|
|
129
|
-
/** Every scrambled id in this checkout → its path (files git knows about and their folders). */
|
|
130
|
-
export function pathMap(s) {
|
|
131
|
-
const map = new Map();
|
|
132
|
-
if (s.mode !== 'blind' || !s.keys)
|
|
133
|
-
return map;
|
|
134
|
-
const files = (git(s.root, ['ls-files', '--cached', '--others', '--exclude-standard']) ?? '').split('\n').filter(Boolean);
|
|
135
|
-
for (const f of files) {
|
|
136
|
-
map.set(scramblePath(s.keys, f), f);
|
|
137
|
-
for (let d = f.lastIndexOf('/'); d > 0; d = f.lastIndexOf('/', d - 1))
|
|
138
|
-
map.set(scramblePath(s.keys, f.slice(0, d)), f.slice(0, d));
|
|
139
|
-
}
|
|
140
|
-
return map;
|
|
141
|
-
}
|
|
142
|
-
/** Text with every k_… id this checkout knows turned back into its path. */
|
|
143
|
-
export const unscramble = (text, map) => (map.size ? text.replace(/k_[0-9a-f]{24}/g, (m) => map.get(m) ?? m) : text);
|
|
144
|
-
/** Paths inside a shell command: the repo folder becomes ".", files and folders that exist are scrambled. */
|
|
145
|
-
export function scrambleCommand(cmd, s, cwd) {
|
|
146
|
-
const rootFwd = s.root.replaceAll('\\', '/');
|
|
147
|
-
let out = cmd.split(s.root).join('.').split(rootFwd).join('.');
|
|
148
|
-
out = out.replace(/[\w@.\-/\\]+/g, (tok) => {
|
|
149
|
-
if (tok.length < 2 || !/[/\\.]/.test(tok) || /^\.+$/.test(tok))
|
|
150
|
-
return tok;
|
|
151
|
-
const rel = repoPath(s.root, tok, cwd);
|
|
152
|
-
if (!rel || rel === '.' || !existsSync(join(s.root, rel)))
|
|
153
|
-
return tok;
|
|
154
|
-
return scramblePath(s.keys, rel);
|
|
155
|
-
});
|
|
156
|
-
return out;
|
|
157
|
-
}
|
|
158
|
-
const CONTENT = ['content', 'old_string', 'new_string', 'edits', 'new_source'];
|
|
159
|
-
/**
|
|
160
|
-
* One event as it may leave a Blind laptop: paths scrambled, file contents and tool output dropped, the folder the
|
|
161
|
-
* agent runs in replaced by ".". Without the key nothing path-like is sent at all.
|
|
162
|
-
*/
|
|
163
|
-
export function blindEvent(e, s) {
|
|
164
|
-
const k = s.keys;
|
|
165
|
-
const cwd = typeof e.cwd === 'string' ? e.cwd : s.root;
|
|
166
|
-
const path = (p) => {
|
|
167
|
-
if (typeof p !== 'string')
|
|
168
|
-
return p;
|
|
169
|
-
const rel = repoPath(s.root, p, cwd);
|
|
170
|
-
return !k ? 'hidden' : rel ? scramblePath(k, rel) : 'outside';
|
|
171
|
-
};
|
|
172
|
-
const d = { ...(e.data ?? {}) };
|
|
173
|
-
switch (e.type) {
|
|
174
|
-
case 'run.start':
|
|
175
|
-
d.project = k ? scrambleName(k, 'r', projectOf(s.root)) : undefined;
|
|
176
|
-
d.cwd = '.';
|
|
177
|
-
if (typeof d.branch === 'string')
|
|
178
|
-
d.branch = k ? scrambleName(k, 'b', d.branch) : undefined;
|
|
179
|
-
delete d.transcript;
|
|
180
|
-
break;
|
|
181
|
-
case 'tool.start': {
|
|
182
|
-
const i = { ...(typeof d.input === 'object' && d.input ? d.input : {}) };
|
|
183
|
-
for (const f of ['file_path', 'notebook_path', 'path'])
|
|
184
|
-
if (i[f] !== undefined)
|
|
185
|
-
i[f] = path(i[f]);
|
|
186
|
-
if (typeof i.command === 'string')
|
|
187
|
-
i.command = k ? scrambleCommand(i.command, s, cwd) : 'hidden';
|
|
188
|
-
for (const f of CONTENT)
|
|
189
|
-
delete i[f];
|
|
190
|
-
d.input = i;
|
|
191
|
-
break;
|
|
192
|
-
}
|
|
193
|
-
case 'tool.end':
|
|
194
|
-
delete d.output; // file contents and command output never leave a Blind laptop
|
|
195
|
-
break;
|
|
196
|
-
case 'turn.end':
|
|
197
|
-
if (d.files && typeof d.files === 'object')
|
|
198
|
-
d.files = k
|
|
199
|
-
? Object.fromEntries(Object.entries(d.files).flatMap(([p, h]) => {
|
|
200
|
-
const rel = repoPath(s.root, p, cwd);
|
|
201
|
-
return rel ? [[scramblePath(k, rel), scrambleHash(k, String(h))]] : [];
|
|
202
|
-
}))
|
|
203
|
-
: undefined;
|
|
204
|
-
break;
|
|
205
|
-
case 'context':
|
|
206
|
-
delete d.text; // what ArcRoom handed over, already unscrambled on this laptop
|
|
207
|
-
break;
|
|
208
|
-
default:
|
|
209
|
-
if (String(e.type).startsWith('other.'))
|
|
210
|
-
return { ...e, cwd: undefined, data: {} };
|
|
211
|
-
}
|
|
212
|
-
for (const [key, v] of Object.entries(d))
|
|
213
|
-
if (v === undefined)
|
|
214
|
-
delete d[key];
|
|
215
|
-
return { ...e, cwd: undefined, data: d };
|
|
216
|
-
}
|