@kuralle-agents/cli 0.14.0 → 0.15.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.
package/dist/fileStore.js CHANGED
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
8
8
  import { dirname } from 'node:path';
9
- import { reviveSession } from '@kuralle-agents/core';
9
+ import { reviveSession, StaleWriteError } from '@kuralle-agents/core';
10
10
  export function fileSessionStore(path) {
11
11
  const readAll = () => {
12
12
  if (!existsSync(path))
@@ -28,8 +28,18 @@ export function fileSessionStore(path) {
28
28
  return map[id] ? reviveSession(map[id]) : null;
29
29
  },
30
30
  async save(session) {
31
+ // Compare-and-swap, matching MemoryStore. The durable journal appends through
32
+ // `mutateSessionWithRetry`, which retries on StaleWriteError — a store that accepts
33
+ // a stale write silently drops the losing append instead, and the step it wrote is
34
+ // then missing when finalizeStep looks for it.
31
35
  const map = readAll();
32
- map[session.id] = session;
36
+ const existing = map[session.id];
37
+ const expected = session.version ?? 0;
38
+ const stored = existing ? (existing.version ?? 0) : 0;
39
+ if (stored !== expected) {
40
+ throw new StaleWriteError(session.id, expected, stored);
41
+ }
42
+ map[session.id] = { ...session, updatedAt: new Date(), version: expected + 1 };
33
43
  writeAll(map);
34
44
  },
35
45
  async delete(id) {
package/dist/send.js CHANGED
@@ -2,6 +2,11 @@
2
2
  * send — ONE turn against a PERSISTED session for adaptive multi-turn conversations.
3
3
  *
4
4
  * Flags: --session <id> · --store <file> · --state · --reset
5
+ * --approve [by] · --deny [by] · --signal <name> [--payload <json>]
6
+ *
7
+ * A `needsApproval` tool suspends the run durably. Without a way to deliver the decision the
8
+ * CLI could enter that pause and never leave it — every later turn re-requested approval and
9
+ * paused again. The runtime already accepted `signalDelivery`; only the CLI could not send one.
5
10
  */
6
11
  import { join } from 'node:path';
7
12
  import { fileSessionStore } from './fileStore.js';
@@ -15,9 +20,39 @@ export async function runSend(argv, buildRuntime) {
15
20
  const storePath = flag(argv, '--store') ?? join(process.cwd(), 'runs/tui-sessions.json');
16
21
  const doReset = argv.includes('--reset');
17
22
  const doState = argv.includes('--state');
18
- const reserved = new Set(['--session', sessionId, '--store', storePath]);
23
+ const approve = argv.includes('--approve');
24
+ const deny = argv.includes('--deny');
25
+ const signalName = flag(argv, '--signal');
26
+ const by = flag(argv, '--approve') ?? flag(argv, '--deny') ?? 'cli';
27
+ const signalDelivery = approve || deny
28
+ ? {
29
+ // A fresh id per delivery: recordSignalDelivery dedupes on it, so reusing one
30
+ // would make a second decision a silent no-op.
31
+ signalId: `cli-${Date.now()}`,
32
+ name: '__approval',
33
+ payload: { approved: approve, by },
34
+ }
35
+ : signalName
36
+ ? {
37
+ signalId: `cli-${Date.now()}`,
38
+ name: signalName,
39
+ payload: JSON.parse(flag(argv, '--payload') ?? '{}'),
40
+ }
41
+ : undefined;
42
+ // Every flag whose VALUE must not be mistaken for message text. Missing `--model` here
43
+ // meant the model id survived the filter and became the first word of every user turn —
44
+ // a whole live run was sent to the model with "gpt-4.1-mini " prepended to each message.
45
+ const consumesValue = new Set([
46
+ '--session',
47
+ '--store',
48
+ '--model',
49
+ '--signal',
50
+ '--payload',
51
+ '--approve',
52
+ '--deny',
53
+ ]);
19
54
  const message = argv
20
- .filter((a, i) => !a.startsWith('--') && !(i > 0 && (argv[i - 1] === '--session' || argv[i - 1] === '--store')))
55
+ .filter((a, i) => !a.startsWith('--') && !(i > 0 && consumesValue.has(argv[i - 1])))
21
56
  .join(' ')
22
57
  .trim();
23
58
  const store = fileSessionStore(storePath);
@@ -37,14 +72,15 @@ export async function runSend(argv, buildRuntime) {
37
72
  console.log(`reset session "${sessionId}"`);
38
73
  return;
39
74
  }
40
- if (doState || !message) {
75
+ // A decision can arrive with no message — approving is itself the turn.
76
+ if (doState || (!message && !signalDelivery)) {
41
77
  console.log(await readState());
42
78
  if (!message && !doState)
43
79
  console.error('(no message — pass one to take a turn, or --state to inspect)');
44
80
  return;
45
81
  }
46
82
  const events = [];
47
- const handle = demo.runtime.run({ sessionId, input: message });
83
+ const handle = demo.runtime.run({ sessionId, input: message, ...(signalDelivery ? { signalDelivery } : {}) });
48
84
  let text = '';
49
85
  for await (const part of handle.events) {
50
86
  if (part.type === 'text-delta') {
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "url": "git+https://github.com/kuralle/kuralle-agents.git",
7
7
  "directory": "packages/cli"
8
8
  },
9
- "version": "0.14.0",
9
+ "version": "0.15.0",
10
10
  "description": "Kuralle CLI — interactive TUI chat, adaptive send, and conversation simulation",
11
11
  "type": "module",
12
12
  "bin": {
@@ -20,18 +20,19 @@
20
20
  "ink-text-input": "^6.0.0",
21
21
  "react": "^19.2.7",
22
22
  "zod": "^4.0.0",
23
- "@kuralle-agents/core": "0.14.0",
24
- "@kuralle-agents/trace-ui": "0.14.0"
23
+ "@kuralle-agents/trace-ui": "0.14.1",
24
+ "@kuralle-agents/core": "0.15.0"
25
25
  },
26
26
  "peerDependencies": {
27
+ "@kuralle-agents/core": "0.x",
27
28
  "ai": "^6.0.0",
28
- "zod": "^4.0.0",
29
- "@kuralle-agents/core": "0.14.0"
29
+ "zod": "^4.0.0"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/node": "^20.11.0",
33
33
  "@types/react": "^19.2.17",
34
- "typescript": "^5.3.0"
34
+ "typescript": "^5.3.0",
35
+ "@kuralle-agents/fs": "0.14.1"
35
36
  },
36
37
  "publishConfig": {
37
38
  "access": "public"
@@ -45,6 +46,7 @@
45
46
  "build": "tsc -p tsconfig.json",
46
47
  "typecheck": "tsc --noEmit -p tsconfig.json",
47
48
  "clean": "rm -rf dist",
48
- "test": "bun test ./test"
49
+ "test": "bun test ./test",
50
+ "typecheck:examples": "tsc --noEmit -p tsconfig.examples.json"
49
51
  }
50
52
  }