archymedes-cli 2.3.0 → 2.5.0

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 (25) hide show
  1. package/dist/{archymedes-aj30app4.mjs → archymedes-06fh0z84.mjs} +370 -159
  2. package/dist/{archymedes-d5wqcmdr.mjs → archymedes-0jxzsh4z.mjs} +97 -97
  3. package/dist/archymedes-130jt0wr.mjs +1231 -0
  4. package/dist/{archymedes-twbks7g3.mjs → archymedes-138genyy.mjs} +214 -5
  5. package/dist/{archymedes-h13ap0za.mjs → archymedes-1g1y6q74.mjs} +1 -1
  6. package/dist/{archymedes-dz59nvw7.mjs → archymedes-2kvjaajw.mjs} +21 -11
  7. package/dist/{archymedes-g8y3rt6t.mjs → archymedes-5vhvv8kr.mjs} +332 -9
  8. package/dist/{archymedes-4kx65anz.mjs → archymedes-a3ms7jvr.mjs} +43 -12
  9. package/dist/{archymedes-tsne2m7p.mjs → archymedes-bmtdqqwt.mjs} +2 -2
  10. package/dist/{archymedes-sf9t1hz9.mjs → archymedes-d3azqkay.mjs} +176 -77
  11. package/dist/archymedes-main.mjs +31312 -28352
  12. package/dist/archymedes-w4bjfye7.mjs +280 -0
  13. package/dist/{editor-screen-g08zzt70.mjs → editor-screen-e01mn7dd.mjs} +49 -14
  14. package/dist/{factory-7vtm2fed.mjs → factory-hyv8agr0.mjs} +326 -326
  15. package/dist/{file-screen-ma9yxq2b.mjs → file-screen-stcxznwy.mjs} +11 -9
  16. package/dist/{guide-screen-gd129txd.mjs → guide-screen-w5nb9sry.mjs} +10 -8
  17. package/dist/{index-qvgcwsvm.mjs → index-teq1cta4.mjs} +1 -1
  18. package/dist/{node-mdgzx68h.mjs → node-0fh63p8z.mjs} +7 -7
  19. package/dist/pager-screen-aamt6xv0.mjs +155 -0
  20. package/dist/projection-x6x6hm9t.mjs +248 -0
  21. package/dist/{sandbox-templates-9jcjws23.mjs → sandbox-templates-8sd1psez.mjs} +2 -2
  22. package/dist/workspace-screen-62k6hnq7.mjs +70 -0
  23. package/package.json +2 -2
  24. package/dist/archymedes-a10k8yrk.mjs +0 -464
  25. package/dist/workspace-screen-8cwk6jz9.mjs +0 -241
@@ -0,0 +1,280 @@
1
+ // packages/core/src/cli/protocol.ts
2
+ import { createHash } from "node:crypto";
3
+ import { promises as fs2 } from "node:fs";
4
+ import path2 from "node:path";
5
+
6
+ // packages/core/src/cli/file-ownership.ts
7
+ import { randomUUID } from "node:crypto";
8
+ import { promises as fs } from "node:fs";
9
+ import os from "node:os";
10
+ import path from "node:path";
11
+
12
+ class OwnershipHeldError extends Error {
13
+ pid;
14
+ constructor(pid) {
15
+ super(`Session is already owned by process ${pid}; close or detach the other writer first.`);
16
+ this.pid = pid;
17
+ }
18
+ }
19
+ async function processStart(pid) {
20
+ if (process.platform !== "linux")
21
+ return;
22
+ try {
23
+ const stat = await fs.readFile(`/proc/${pid}/stat`, "utf8");
24
+ const boot = await fs.readFile("/proc/sys/kernel/random/boot_id", "utf8");
25
+ return `${boot.trim()}:${stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19]}`;
26
+ } catch {
27
+ return;
28
+ }
29
+ }
30
+ async function readOwner(file) {
31
+ try {
32
+ const owner = JSON.parse(await fs.readFile(file, "utf8"));
33
+ if (!owner || typeof owner.token !== "string" || !Number.isSafeInteger(owner.pid) || owner.pid < 1 || typeof owner.host !== "string") {
34
+ throw new Error(`Invalid ownership record: ${file}`);
35
+ }
36
+ return owner;
37
+ } catch (error) {
38
+ if (error.code === "ENOENT")
39
+ return null;
40
+ throw error;
41
+ }
42
+ }
43
+ async function isAlive(owner) {
44
+ if (owner.host !== os.hostname())
45
+ return true;
46
+ try {
47
+ process.kill(owner.pid, 0);
48
+ } catch (error) {
49
+ return error.code !== "ESRCH";
50
+ }
51
+ const started = await processStart(owner.pid);
52
+ return !owner.started || !started || owner.started === started;
53
+ }
54
+ async function acquireFileOwnership(file, depth = 0) {
55
+ if (depth > 8)
56
+ throw new Error(`Ownership recovery requires inspection: ${file}`);
57
+ await fs.mkdir(path.dirname(file), { recursive: true });
58
+ const owner = { token: randomUUID(), pid: process.pid, host: os.hostname(), started: await processStart(process.pid) };
59
+ const candidate = `${file}.${owner.token}.tmp`;
60
+ const handle = await fs.open(candidate, "wx", 384);
61
+ try {
62
+ await handle.writeFile(JSON.stringify(owner));
63
+ await handle.sync();
64
+ } finally {
65
+ await handle.close();
66
+ }
67
+ try {
68
+ for (let attempt = 0;attempt < 8; attempt++) {
69
+ try {
70
+ await fs.link(candidate, file);
71
+ return async () => {
72
+ if ((await readOwner(file))?.token === owner.token)
73
+ await fs.unlink(file);
74
+ };
75
+ } catch (error) {
76
+ if (error.code !== "EEXIST")
77
+ throw error;
78
+ }
79
+ const current = await readOwner(file);
80
+ if (!current)
81
+ continue;
82
+ if (await isAlive(current))
83
+ throw new OwnershipHeldError(current.pid);
84
+ const release = await acquireFileOwnership(`${file}.reap`, depth + 1);
85
+ try {
86
+ if ((await readOwner(file))?.token === current.token)
87
+ await fs.unlink(file);
88
+ } finally {
89
+ await release();
90
+ }
91
+ }
92
+ throw new Error(`Could not acquire session ownership: ${file}`);
93
+ } finally {
94
+ await fs.unlink(candidate).catch(() => {
95
+ return;
96
+ });
97
+ }
98
+ }
99
+
100
+ // packages/core/src/cli/protocol.ts
101
+ var ARCHYMEDES_PROTOCOL_VERSION = 1;
102
+ var JOURNAL_GENESIS_HASH = "0".repeat(64);
103
+ var TRANSITIONS = {
104
+ queued: ["running", "cancelled", "failed"],
105
+ running: ["waiting_approval", "completed", "blocked", "needs_verification", "cancelled", "failed", "iteration_limit"],
106
+ waiting_approval: ["running", "blocked", "cancelled", "failed"],
107
+ completed: [],
108
+ blocked: [],
109
+ needs_verification: [],
110
+ cancelled: [],
111
+ failed: [],
112
+ iteration_limit: []
113
+ };
114
+ function assertTurnTransition(from, to) {
115
+ if (!TRANSITIONS[from].includes(to))
116
+ throw new Error(`Illegal Archymedes turn transition: ${from} -> ${to}`);
117
+ }
118
+ var SENSITIVE_KEY = /(authorization|cookie|credential|password|private.?key|secret|token)/i;
119
+ var SECRET_ASSIGNMENT = /((?:[A-Za-z0-9_]*(?:api[_-]?key|access[_-]?key(?:_id)?|authorization|credential|password|passwd|private[_-]?key|secret|token)[A-Za-z0-9_]*)\s*[:=]\s*)([^\s,;]+)/gi;
120
+ var PRIVATE_KEY_BLOCK = /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/g;
121
+ var AUTHENTICATED_URL = /([a-z][a-z0-9+.-]*:\/\/)([^/\s:@]+):([^@\s/]+)@/gi;
122
+ function redactJournalString(value) {
123
+ return value.replace(PRIVATE_KEY_BLOCK, "[REDACTED PRIVATE KEY]").replace(AUTHENTICATED_URL, "$1[REDACTED]@").replace(SECRET_ASSIGNMENT, "$1[REDACTED]");
124
+ }
125
+ function boundedJournalValue(value, key = "") {
126
+ if (SENSITIVE_KEY.test(key))
127
+ return "[REDACTED]";
128
+ if (typeof value === "string") {
129
+ const redacted = redactJournalString(value);
130
+ if (redacted.length <= 4000)
131
+ return redacted;
132
+ const digest = createHash("sha256").update(value).digest("hex");
133
+ return `${redacted.slice(0, 4000)}
134
+ ...[${redacted.length - 4000} chars omitted; original sha256=${digest}]`;
135
+ }
136
+ if (Array.isArray(value))
137
+ return value.map((item) => boundedJournalValue(item));
138
+ if (value && typeof value === "object") {
139
+ return Object.fromEntries(Object.entries(value).map(([childKey, item]) => [childKey, boundedJournalValue(item, childKey)]));
140
+ }
141
+ return value;
142
+ }
143
+ function runtimeEventForJournal(event) {
144
+ if (event.type === "tool_call")
145
+ return { ...event, arguments: boundedJournalValue(event.arguments) };
146
+ if (event.type === "tool_result") {
147
+ return {
148
+ ...event,
149
+ content: boundedJournalValue(event.content),
150
+ ...event.data ? { data: boundedJournalValue(event.data) } : {}
151
+ };
152
+ }
153
+ return event;
154
+ }
155
+ function hashEnvelope(envelope) {
156
+ return createHash("sha256").update(JSON.stringify(envelope)).digest("hex");
157
+ }
158
+ function eventJournalPath(root, sessionId) {
159
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(sessionId))
160
+ throw new Error("Session id contains unsafe characters");
161
+ return path2.join(root, ".archymedes", "events", `${sessionId}.jsonl`);
162
+ }
163
+ async function readEventJournal(root, sessionId) {
164
+ return (await readJournalFile(root, sessionId)).events;
165
+ }
166
+ async function readJournalFile(root, sessionId) {
167
+ const file = eventJournalPath(root, sessionId);
168
+ let text;
169
+ try {
170
+ text = await fs2.readFile(file, "utf8");
171
+ } catch (error) {
172
+ if (error.code === "ENOENT")
173
+ return { events: [], text: "", completeText: "" };
174
+ throw error;
175
+ }
176
+ const completeText = text.endsWith(`
177
+ `) ? text : text.slice(0, text.lastIndexOf(`
178
+ `) + 1);
179
+ const events = completeText.split(`
180
+ `).filter(Boolean).map((line) => JSON.parse(line));
181
+ let previousHash = JOURNAL_GENESIS_HASH;
182
+ for (let index = 0;index < events.length; index += 1) {
183
+ const event = events[index];
184
+ if (event.protocolVersion !== ARCHYMEDES_PROTOCOL_VERSION)
185
+ throw new Error(`Unsupported Archymedes protocol version ${event.protocolVersion}`);
186
+ if (event.sessionId !== sessionId || event.sequence !== index + 1 || event.previousHash !== previousHash) {
187
+ throw new Error(`Invalid Archymedes event chain at sequence ${event.sequence}`);
188
+ }
189
+ const { hash, ...withoutHash } = event;
190
+ if (hash !== hashEnvelope(withoutHash))
191
+ throw new Error(`Archymedes event integrity check failed at sequence ${event.sequence}`);
192
+ previousHash = hash;
193
+ }
194
+ return { events, text, completeText };
195
+ }
196
+
197
+ class EventJournal {
198
+ root;
199
+ sessionId;
200
+ handle = null;
201
+ releaseOwnership;
202
+ closed = false;
203
+ sequence = 0;
204
+ previousHash = JOURNAL_GENESIS_HASH;
205
+ initialized = null;
206
+ tail = Promise.resolve();
207
+ constructor(root, sessionId) {
208
+ this.root = root;
209
+ this.sessionId = sessionId;
210
+ }
211
+ async initialize() {
212
+ this.releaseOwnership = await acquireFileOwnership(`${eventJournalPath(this.root, this.sessionId)}.owner`);
213
+ try {
214
+ const { events, text, completeText } = await readJournalFile(this.root, this.sessionId);
215
+ this.sequence = events.length;
216
+ this.previousHash = events.at(-1)?.hash ?? JOURNAL_GENESIS_HASH;
217
+ const file = eventJournalPath(this.root, this.sessionId);
218
+ await fs2.mkdir(path2.dirname(file), { recursive: true });
219
+ if (text.length !== completeText.length)
220
+ await fs2.truncate(file, Buffer.byteLength(completeText, "utf8"));
221
+ this.handle = await fs2.open(file, "a", 384);
222
+ } catch (error) {
223
+ await this.releaseOwnership();
224
+ this.releaseOwnership = undefined;
225
+ throw error;
226
+ }
227
+ }
228
+ async open() {
229
+ if (this.closed)
230
+ throw new Error("Session journal is closed");
231
+ this.initialized ??= this.initialize();
232
+ await this.initialized;
233
+ }
234
+ append(payload, options = {}) {
235
+ if (this.closed)
236
+ return Promise.reject(new Error("Session journal is closed"));
237
+ let written;
238
+ const operation = this.tail.then(async () => {
239
+ await this.open();
240
+ const withoutHash = {
241
+ protocolVersion: ARCHYMEDES_PROTOCOL_VERSION,
242
+ sequence: this.sequence + 1,
243
+ sessionId: this.sessionId,
244
+ timestamp: new Date().toISOString(),
245
+ previousHash: this.previousHash,
246
+ payload
247
+ };
248
+ written = { ...withoutHash, hash: hashEnvelope(withoutHash) };
249
+ await this.handle.write(`${JSON.stringify(written)}
250
+ `);
251
+ if (options.durable)
252
+ await this.handle.sync();
253
+ this.sequence = written.sequence;
254
+ this.previousHash = written.hash;
255
+ });
256
+ this.tail = operation;
257
+ return operation.then(() => written);
258
+ }
259
+ async flush() {
260
+ await this.tail;
261
+ await this.handle?.sync();
262
+ }
263
+ async close() {
264
+ try {
265
+ await this.initialized;
266
+ await this.flush();
267
+ } finally {
268
+ this.closed = true;
269
+ try {
270
+ await this.handle?.close();
271
+ } finally {
272
+ this.handle = null;
273
+ await this.releaseOwnership?.();
274
+ this.releaseOwnership = undefined;
275
+ }
276
+ }
277
+ }
278
+ }
279
+
280
+ export { OwnershipHeldError, acquireFileOwnership, assertTurnTransition, runtimeEventForJournal, eventJournalPath, readEventJournal, EventJournal };
@@ -1,20 +1,21 @@
1
1
  import {
2
2
  diffLines,
3
3
  languageOf
4
- } from "./archymedes-dz59nvw7.mjs";
5
- import"./archymedes-tsne2m7p.mjs";
4
+ } from "./archymedes-2kvjaajw.mjs";
5
+ import"./archymedes-bmtdqqwt.mjs";
6
6
  import {
7
- NO_COLOR_PALETTE
8
- } from "./archymedes-a10k8yrk.mjs";
7
+ NO_COLOR_PALETTE,
8
+ exitTopTermUIApp
9
+ } from "./archymedes-130jt0wr.mjs";
9
10
  import {
10
11
  __require
11
12
  } from "./archymedes-x67v3n58.mjs";
12
13
 
13
- // src/ui/editor-screen.tsx
14
- import { useInput, useState } from "@termuijs/jsx";
14
+ // packages/archymedes-cli/src/ui/editor-screen.tsx
15
+ import { useEffect, useInput, useRef, useState } from "@termuijs/jsx";
15
16
  import { Box, Text } from "@termuijs/widgets";
16
17
 
17
- // src/ui/editor.ts
18
+ // packages/archymedes-cli/src/ui/editor.ts
18
19
  function initialEditorState(path, content, viewportRows = 20) {
19
20
  const lines = content.split(`
20
21
  `);
@@ -79,6 +80,15 @@ function keyToEditorAction(key, character, state) {
79
80
  return { kind: "save" };
80
81
  if (key.ctrl && (name === "q" || name === "c"))
81
82
  return { kind: "quit" };
83
+ if (state.confirmLeave) {
84
+ if (character === "y" || character === "Y" || isEnter)
85
+ return { kind: "save" };
86
+ if (character === "n" || character === "N")
87
+ return { kind: "quit" };
88
+ if (isEscape)
89
+ return { kind: "cancelLeave" };
90
+ return { kind: "none" };
91
+ }
82
92
  if (state.search.typing) {
83
93
  if (isEscape)
84
94
  return { kind: "searchCommit" };
@@ -116,7 +126,7 @@ function keyToEditorAction(key, character, state) {
116
126
  if (state.pending === "g")
117
127
  return name === "g" || character === "g" ? { kind: "jump", to: "fileStart" } : { kind: "normal" };
118
128
  if (isEscape)
119
- return { kind: "normal" };
129
+ return { kind: "leave" };
120
130
  if (name === "up" || character === "k")
121
131
  return { kind: "move", rows: -1 };
122
132
  if (name === "down" || character === "j")
@@ -269,7 +279,15 @@ function applyEditorAction(state, action) {
269
279
  case "save":
270
280
  return { state: clamp({ ...state, dirty: false, message: `saved ${state.path}`, pending: undefined }), effect: { kind: "save" } };
271
281
  case "quit":
272
- return { state, effect: { kind: "quit" } };
282
+ return { state: { ...state, confirmLeave: false }, effect: { kind: "quit" } };
283
+ case "leave":
284
+ if (!state.dirty)
285
+ return { state, effect: { kind: "quit" } };
286
+ return { state: { ...state, pending: undefined, confirmLeave: true, message: "Save changes? y yes · n discard · Esc keep editing" }, effect: undefined };
287
+ case "cancelLeave":
288
+ return plain({ ...state, confirmLeave: false, message: undefined });
289
+ case "autosaved":
290
+ return editorContent(state) === action.content ? plain({ ...state, dirty: false, message: "auto-saved" }) : plain(state);
273
291
  case "search":
274
292
  return plain({ ...state, search: { query: "", typing: true }, message: undefined });
275
293
  case "searchType":
@@ -311,7 +329,8 @@ function editorKeyBar(state, columns = Number.POSITIVE_INFINITY) {
311
329
  { text: "u undo" },
312
330
  { text: "/ find" },
313
331
  { text: "^S save", essential: true },
314
- { text: "^Q quit", essential: true }
332
+ { text: "^Q quit", essential: true },
333
+ { text: "esc back", essential: true }
315
334
  ];
316
335
  const optional = hints.filter((hint) => !hint.essential);
317
336
  for (let dropped = 0;dropped <= optional.length; dropped += 1) {
@@ -347,7 +366,7 @@ function caretInto(text, cursorCol) {
347
366
  return `${padded.slice(0, cursorCol)}█${padded.slice(cursorCol + 1)}`;
348
367
  }
349
368
 
350
- // src/ui/explain-view.ts
369
+ // packages/archymedes-cli/src/ui/explain-view.ts
351
370
  var PATTERNS = {
352
371
  typescript: {
353
372
  imports: /^\s*import\b.*from\s+["'](.+)["']/,
@@ -531,7 +550,7 @@ function composeExplainPanel(input, width, height, colors = DEFAULT_EXPLAIN_COLO
531
550
  return padded;
532
551
  }
533
552
 
534
- // src/ui/editor-screen.tsx
553
+ // packages/archymedes-cli/src/ui/editor-screen.tsx
535
554
  import { jsxDEV } from "@termuijs/jsx/jsx-dev-runtime";
536
555
  var Panel = Box;
537
556
  var Line = Text;
@@ -543,8 +562,22 @@ function panelWidthFor(columns) {
543
562
  return 0;
544
563
  return Math.min(MAX_PANEL_COLUMNS, Math.floor(columns * 0.38), columns - MIN_CODE_COLUMNS);
545
564
  }
546
- function EditorScreen({ columns, rows, path, content, onExit, palette = NO_COLOR_PALETTE, explain }) {
565
+ function EditorScreen({ columns, rows, path, content, onExit, palette = NO_COLOR_PALETTE, explain, autosave, autosaveDelayMs = 1000 }) {
547
566
  const [state, setState] = useState(() => initialEditorState(path, content, Math.max(1, rows - CHROME_ROWS)));
567
+ const timer = useRef(undefined);
568
+ const text = editorContent(state);
569
+ useEffect(() => {
570
+ if (!autosave || !state.dirty || state.confirmLeave)
571
+ return;
572
+ const pending = text;
573
+ timer.current = setTimeout(() => {
574
+ autosave(pending).then(() => setState((current) => applyEditorAction(current, { kind: "autosaved", content: pending }).state), () => setState((current) => ({ ...current, message: "auto-save failed — Ctrl+S to retry" })));
575
+ }, autosaveDelayMs);
576
+ return () => {
577
+ if (timer.current)
578
+ clearTimeout(timer.current);
579
+ };
580
+ }, [text, state.dirty, state.confirmLeave]);
548
581
  const [panel, setPanel] = useState(() => initialExplainPanelState());
549
582
  const panelWidth = panel.open ? panelWidthFor(columns) : 0;
550
583
  const codeWidth = panelWidth > 0 ? columns - panelWidth - 1 : columns;
@@ -554,7 +587,7 @@ function EditorScreen({ columns, rows, path, content, onExit, palette = NO_COLOR
554
587
  return;
555
588
  }
556
589
  setPanel((current) => withAiExplanation(current, { status: "loading" }));
557
- explain(currentContent, path).then((text) => setPanel((current) => withAiExplanation(current, { status: "ready", text })), (error) => setPanel((current) => withAiExplanation(current, { status: "error", message: error instanceof Error ? error.message : String(error) })));
590
+ explain(currentContent, path).then((text2) => setPanel((current) => withAiExplanation(current, { status: "ready", text: text2 })), (error) => setPanel((current) => withAiExplanation(current, { status: "error", message: error instanceof Error ? error.message : String(error) })));
558
591
  };
559
592
  useInput((input, key) => {
560
593
  if (key?.ctrl && key?.key === "e") {
@@ -629,6 +662,7 @@ async function runEditorScreen(options) {
629
662
  if (settled)
630
663
  return;
631
664
  settled = true;
665
+ exitTopTermUIApp();
632
666
  resolve(saved);
633
667
  };
634
668
  renderApp(EditorScreen, {
@@ -638,6 +672,7 @@ async function runEditorScreen(options) {
638
672
  content: options.content,
639
673
  palette: options.palette,
640
674
  explain: options.explain,
675
+ autosave: options.autosave,
641
676
  onExit: finish,
642
677
  fullscreen: true
643
678
  }).catch(() => finish(undefined));