@paradow/ledge 0.12.1 → 0.13.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.
@@ -1,13 +1,14 @@
1
1
  import {
2
2
  attachEnv
3
- } from "./chunk-ZNRXLDSH.js";
3
+ } from "./chunk-ZSKCV73V.js";
4
4
  import {
5
5
  connect
6
- } from "./chunk-XXFUYME7.js";
7
- import "./chunk-X34LPKG7.js";
6
+ } from "./chunk-SVVQ5KPJ.js";
7
+ import "./chunk-5XFBRTKO.js";
8
8
  import {
9
- PROTOCOL_PORT
10
- } from "./chunk-VY45MACP.js";
9
+ PROTOCOL_PORT,
10
+ backendOf
11
+ } from "./chunk-PPRB4XIX.js";
11
12
  import {
12
13
  realTmux
13
14
  } from "./chunk-ZEHTNFPV.js";
@@ -24,6 +25,17 @@ function attachInherit(args, env) {
24
25
  child.on("exit", (c) => resolve(c ?? 1));
25
26
  });
26
27
  }
28
+ async function nativeSession(id, port) {
29
+ try {
30
+ const c = await connect({ port, timeoutMs: 800 });
31
+ const snap = await c.next("snapshot");
32
+ await Promise.race([c.close(), new Promise((r) => setTimeout(r, 500))]);
33
+ const s = snap.sessions.find((x) => x.id === id);
34
+ return s && backendOf(s) === "omp-rpc" ? s.name : null;
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
27
39
  async function attach(id, opts = {}) {
28
40
  if (!id) {
29
41
  console.error("ledge attach: missing session id");
@@ -32,7 +44,8 @@ async function attach(id, opts = {}) {
32
44
  const tmux = realTmux(opts.tmuxSocket ?? TMUX_SOCKET);
33
45
  const tmuxName = `ledge-${id}`;
34
46
  if (!await tmux.hasSession(tmuxName)) {
35
- console.error(`ledge attach: no session ${id}`);
47
+ const native = await nativeSession(id, opts.port ?? PROTOCOL_PORT);
48
+ console.error(native ? `ledge attach: ${native} is driven by Ledge over OMP's own protocol: there is no terminal to attach to (open a shell from the app)` : `ledge attach: no session ${id}`);
36
49
  return 1;
37
50
  }
38
51
  let title = tmuxName;
@@ -7,7 +7,7 @@ import {
7
7
  parseClientMessage,
8
8
  parseServerMessage,
9
9
  readDaemonToken
10
- } from "./chunk-VY45MACP.js";
10
+ } from "./chunk-PPRB4XIX.js";
11
11
  import "./chunk-FGOFRWIB.js";
12
12
 
13
13
  // src/cli/bridge.ts
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  pickName
3
- } from "./chunk-VY45MACP.js";
3
+ } from "./chunk-PPRB4XIX.js";
4
4
  import {
5
5
  paths
6
6
  } from "./chunk-FGOFRWIB.js";
@@ -126,7 +126,7 @@ function claudeToolAnswered(path, id) {
126
126
  // src/spawn.ts
127
127
  import { basename, join as join2 } from "path";
128
128
  import { randomBytes, randomUUID } from "crypto";
129
- var AGENT_BIN = { claude: "claude", codex: "codex", cursor: "cursor-agent" };
129
+ var AGENT_BIN = { claude: "claude", codex: "codex", cursor: "cursor-agent", omp: "omp" };
130
130
  var CODEX_ENV_VAR = "LEDGE_CODEX_SESSION";
131
131
  var CODEX_HOOK_VAR = "LEDGE_CODEX_HOOK_SESSION";
132
132
  function agentIdArgs(tool, id) {
@@ -190,6 +190,7 @@ export {
190
190
  CODEX_ENV_VAR,
191
191
  CODEX_HOOK_VAR,
192
192
  exitFile,
193
+ PARENT_AGENT_VARS,
193
194
  agentEnv,
194
195
  spawnSession
195
196
  };
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  connect
3
- } from "./chunk-XXFUYME7.js";
3
+ } from "./chunk-SVVQ5KPJ.js";
4
4
  import {
5
5
  isArchived
6
- } from "./chunk-VY45MACP.js";
6
+ } from "./chunk-PPRB4XIX.js";
7
7
 
8
8
  // src/cli/who.ts
9
9
  import { basename } from "path";
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  connect,
3
3
  sendMessageAndAwaitAck
4
- } from "./chunk-XXFUYME7.js";
4
+ } from "./chunk-SVVQ5KPJ.js";
5
5
  import {
6
6
  CODEX_HOOK_VAR
7
- } from "./chunk-X34LPKG7.js";
7
+ } from "./chunk-5XFBRTKO.js";
8
8
 
9
9
  // src/cli/send.ts
10
10
  import { randomUUID } from "crypto";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  cleanTopic,
3
3
  sessionTitle
4
- } from "./chunk-VY45MACP.js";
4
+ } from "./chunk-PPRB4XIX.js";
5
5
 
6
6
  // src/daemon/crossSession.ts
7
7
  var FIRST_LINE = /^Another Claude session sent a message:\s*\n/;
@@ -101,6 +101,9 @@ function pendingFor(ev) {
101
101
  if (ev.name === "Notification" && ev.kind === "permission") {
102
102
  return { kind: "permission", text: ev.text ?? "", source: "hook" };
103
103
  }
104
+ if (ev.name === "NativeRequest") {
105
+ return { kind: "question", text: ev.text ?? "", ...ev.options?.length ? { options: ev.options } : {}, ...ev.questions ? { questions: ev.questions } : {}, ...ev.requestId ? { requestId: ev.requestId } : {}, source: "rpc", tool: "omp" };
106
+ }
104
107
  if (ev.name === "PanePrompt") {
105
108
  return { kind: ev.kind ?? "permission", text: ev.text ?? "", ...ev.body ? { body: ev.body } : {}, options: ev.options, ...details(ev), source: "pane" };
106
109
  }
@@ -295,6 +298,16 @@ function reduceEvent(s, ev) {
295
298
  const state = pending.kind === "question" ? "waiting_input" : "waiting_permission";
296
299
  return { ...s, state, pending, lastActivityAt: at };
297
300
  }
301
+ case "QueueUpdate": {
302
+ const { queue: _q, ...rest } = s;
303
+ return ev.queue?.length ? { ...rest, queue: ev.queue } : s.queue ? rest : s;
304
+ }
305
+ case "AgentMessage":
306
+ return s.lastActivityAt === at ? s : { ...s, lastActivityAt: at };
307
+ case "NativeRequest": {
308
+ if (s.pending) return s;
309
+ return { ...s, state: "waiting_input", pending: pendingFor(ev), lastActivityAt: at };
310
+ }
298
311
  case "SessionStart":
299
312
  case "UserPromptSubmit":
300
313
  case "PreToolUse":
@@ -302,6 +315,7 @@ function reduceEvent(s, ev) {
302
315
  case "Notification": {
303
316
  const notice2 = ev.name === "UserPromptSubmit" && !!ev.text && !!crossSessionNotices(ev.text);
304
317
  if (ev.name === "UserPromptSubmit" && ev.text && !s.topic && !notice2 && !crossSession(ev.text)) s = withTopic(s, ev.text, "prompt");
318
+ if (s.pending?.source === "rpc" && ev.name === "PreToolUse") return { ...s, lastHookAt: at, lastActivityAt: at };
305
319
  const incoming = pendingFor(ev);
306
320
  if (incoming) {
307
321
  if (ev.name === "Notification" && s.pending) return { ...s, lastHookAt: at, lastActivityAt: at };
@@ -0,0 +1,6 @@
1
+ // src/version.ts
2
+ var LEDGE_VERSION = true ? "0.13.0" : "0.0.0-dev";
3
+
4
+ export {
5
+ LEDGE_VERSION
6
+ };
@@ -4,8 +4,16 @@ import {
4
4
 
5
5
  // ../protocol/src/session.ts
6
6
  import { z } from "zod";
7
- var ToolSchema = z.enum(["claude", "codex", "cursor"]);
7
+ var ToolSchema = z.enum(["claude", "codex", "cursor", "omp"]);
8
+ var BackendSchema = z.enum(["tmux", "omp-rpc"]);
8
9
  var SessionStateSchema = z.enum(["starting", "working", "waiting_input", "waiting_permission", "idle", "exited", "error"]);
10
+ var AskQuestionSchema = z.object({
11
+ id: z.string(),
12
+ question: z.string(),
13
+ header: z.string().optional(),
14
+ multi: z.boolean().optional(),
15
+ options: z.array(z.object({ label: z.string(), description: z.string().optional() }))
16
+ });
9
17
  var PendingSchema = z.object({
10
18
  kind: z.enum(["question", "permission", "plan", "diff"]),
11
19
  text: z.string(),
@@ -15,7 +23,11 @@ var PendingSchema = z.object({
15
23
  options: z.array(z.string()).optional(),
16
24
  /** Each option's description, by index ('' when it has none); left out when no option has one. */
17
25
  optionDetails: z.array(z.string()).optional(),
18
- source: z.enum(["hook", "pane"]),
26
+ /** `rpc`: the harness's own protocol asked (OMP): answered on its request id, never by typing. */
27
+ source: z.enum(["hook", "pane", "rpc"]),
28
+ /** OMP: every question of one `ask` call, answered together with `session.answer`; `requestId` is the dialog it answers. */
29
+ questions: z.array(AskQuestionSchema).optional(),
30
+ requestId: z.string().optional(),
19
31
  /** Set when the request comes from a helper (subagent): it is routed through its parent. */
20
32
  helperId: z.string().optional(),
21
33
  /** For a helper-routed request: the state the parent returns to once it is answered (main thread may stop meanwhile). */
@@ -60,9 +72,13 @@ var SessionSchema = z.object({
60
72
  /** The folder the agent works in now, as its last hook reported it: `cwd` stays where it started, this follows a worktree it entered. */
61
73
  workdir: z.string().optional(),
62
74
  tmuxSession: z.string().nullable(),
75
+ /** Left out for a tmux session. `omp-rpc`: no pane, nothing to attach to; the daemon holds the process (see `agentSessionFile`). */
76
+ backend: BackendSchema.optional(),
63
77
  state: SessionStateSchema,
64
78
  pending: PendingSchema.optional(),
65
79
  helpers: z.array(HelperSchema).default([]),
80
+ /** OMP: the instructions waiting in its own queue (typed while it works), as it reports them; left out when none. */
81
+ queue: z.array(z.object({ text: z.string(), mode: z.enum(["steer", "follow_up"]) })).optional(),
66
82
  lastReply: ReplySchema.optional(),
67
83
  /** The last message this session sent to another agent: its tool_use_id, the address, the peer's Ledge session id, a one-line summary. */
68
84
  lastMessage: z.object({ id: z.string(), to: z.string(), peer: z.string().optional(), summary: z.string(), at: z.string().datetime() }).optional(),
@@ -72,8 +88,12 @@ var SessionSchema = z.object({
72
88
  /** The model and effort Ledge launched the agent with (#58); left out, the agent's own default. */
73
89
  model: z.string().optional(),
74
90
  effort: z.string().optional(),
91
+ /** A model change asked while the agent worked (`session.model`), applied at its next free moment; a field left out is the agent's default. */
92
+ modelNext: z.object({ model: z.string().optional(), effort: z.string().optional() }).optional(),
75
93
  /** The agent's own session id when Ledge chose it at launch (Claude Code `--session-id`). */
76
94
  agentSessionId: z.string().optional(),
95
+ /** OMP: the exact journal file of the conversation, what a resume names (never "the latest of the folder"). */
96
+ agentSessionFile: z.string().optional(),
77
97
  /** The agent's transcript: the latest one a hook named (it follows /clear and --resume), else the one the launch implies. */
78
98
  transcriptPath: z.string().optional(),
79
99
  exitCode: z.number().int().nullable().optional(),
@@ -85,6 +105,9 @@ var SessionSchema = z.object({
85
105
  /** When the session was last taken out of the archive: the poller's retirement clock restarts from it, not from `exitedAt`. */
86
106
  restoredAt: z.string().datetime().optional()
87
107
  });
108
+ function backendOf(s) {
109
+ return s.backend ?? "tmux";
110
+ }
88
111
  function isArchived(s) {
89
112
  return s.archivedAt !== void 0;
90
113
  }
@@ -279,8 +302,9 @@ var IslandScaleSchema = z10.union([z10.literal("auto"), z10.literal(1), z10.lite
279
302
  var IslandPrefsSchema = z10.object({
280
303
  /** Shown at the top of the screen; hidden, Desktop still notifies. */
281
304
  visible: z10.boolean(),
282
- /** The primary screen, or the first other one. */
283
- screen: z10.enum(["primary", "second"]),
305
+ /** `primary` (whichever it is after a reconfiguration), or a monitor's id as the shell lists it (`list_monitors`: the OS
306
+ * name, `#index` without one). `second`, written by older fronts, is the first monitor that is not the primary. */
307
+ screen: z10.string().trim().min(1).max(200),
284
308
  position: z10.enum(["center", "left", "right"]),
285
309
  /** A strip under a maximised window. */
286
310
  discreet: z10.boolean(),
@@ -406,7 +430,9 @@ var NOTE_NOT_TYPED = "note_not_typed";
406
430
  var DECISION_OFFERS = {
407
431
  claude: { always: ["permission", "diff", "plan"], note: ["permission", "diff", "plan"] },
408
432
  codex: { always: ["permission", "diff"], note: ["permission", "diff", "plan"] },
409
- cursor: { always: [], note: ["permission", "plan"] }
433
+ cursor: { always: [], note: ["permission", "plan"] },
434
+ /** OMP answers its own native requests (spec 2026-10-20 §3.3): no « always », no note until those are typed. */
435
+ omp: { always: [], note: [] }
410
436
  };
411
437
  function offers(tool, kind, what) {
412
438
  return DECISION_OFFERS[tool][what].includes(kind);
@@ -430,7 +456,7 @@ function pendingKey(p) {
430
456
  // ../protocol/src/messages.ts
431
457
  var PROTOCOL_PORT = 47831;
432
458
  var SessionEventSchema = z14.object({
433
- name: z14.enum(["SessionStart", "UserPromptSubmit", "PreToolUse", "PermissionRequest", "Notification", "Stop", "SessionEnd", "PaneChanged", "PaneGone", "PanePrompt", "PaneIdle", "InjectFailed", "Decided", "PostToolUse", "PostToolUseFailure", "StopFailure", "SubagentStart", "SubagentStop", "PaneTitle", "Renamed"]),
459
+ name: z14.enum(["SessionStart", "UserPromptSubmit", "PreToolUse", "PermissionRequest", "Notification", "Stop", "SessionEnd", "PaneChanged", "PaneGone", "PanePrompt", "PaneIdle", "InjectFailed", "Decided", "PostToolUse", "PostToolUseFailure", "StopFailure", "SubagentStart", "SubagentStop", "PaneTitle", "Renamed", "NativeRequest", "AgentMessage", "QueueUpdate"]),
434
460
  at: z14.string().datetime(),
435
461
  source: z14.enum(["hook", "pane", "daemon"]),
436
462
  tool: z14.string().optional(),
@@ -447,6 +473,16 @@ var SessionEventSchema = z14.object({
447
473
  task: z14.string().optional(),
448
474
  /** PostToolUseFailure: the user interrupted the turn (Esc); Claude then waits for input with no Stop. */
449
475
  interrupt: z14.boolean().optional(),
476
+ /** QueueUpdate: OMP's queue as it stands. */
477
+ queue: z14.array(z14.object({ text: z14.string(), mode: z14.enum(["steer", "follow_up"]) })).optional(),
478
+ /** AgentMessage: the harness's own message, its text so far (`streaming`) or whole. `messageId` is unique for the session's life. */
479
+ messageId: z14.string().optional(),
480
+ streaming: z14.boolean().optional(),
481
+ /** NativeRequest: the dialog's id, and OMP's `ask` questions when it is one. */
482
+ requestId: z14.string().optional(),
483
+ questions: z14.array(AskQuestionSchema).optional(),
484
+ /** Stop: its text is already in the thread as AgentMessage cards, which the reply card must not repeat. */
485
+ streamed: z14.boolean().optional(),
450
486
  /** Native tracking (spec 2026-10-13 §2): the harness's id of the tool call, so its result lands on the same row. */
451
487
  toolUseId: z14.string().optional(),
452
488
  /** The tool's input as the harness gave it, every string capped by the hook. */
@@ -474,6 +510,10 @@ var DaemonInfoSchema = z14.object({
474
510
  archive: z14.boolean().optional(),
475
511
  /** Takes `model` and `effort` on `session.launch` and answers `models.list` (#58). */
476
512
  models: z14.boolean().optional(),
513
+ /** Drives OMP over its RPC: launching `omp` sessions is possible when the binary is there (`models.list` says so). */
514
+ omp: z14.boolean().optional(),
515
+ /** Takes `session.model`: a live session's model and effort changed (#58, lot 2). */
516
+ modelSwitch: z14.boolean().optional(),
477
517
  /** Its home folder, so a client shows the folders under it as `~` (absent from older daemons). */
478
518
  home: z14.string().optional()
479
519
  });
@@ -490,6 +530,8 @@ var ServerMessageSchema = z14.discriminatedUnion("type", [
490
530
  * from daemons older than the auto-update. */
491
531
  z14.object({ type: z14.literal("snapshot"), sessions: z14.array(SessionSchema), recents: z14.array(RecentSchema).optional(), launcher: z14.array(z14.string()).optional(), daemon: DaemonInfoSchema.optional(), sshAlias: SshAliasSchema.optional() }),
492
532
  z14.object({ type: z14.literal("session.updated"), session: SessionSchema }),
533
+ /** OMP: the instructions taken back from its queue by an interrupt that restores it; the asker puts them back in its draft. */
534
+ z14.object({ type: z14.literal("session.queue.restored"), id: z14.string(), texts: z14.array(z14.string()) }),
493
535
  z14.object({ type: z14.literal("session.removed"), id: z14.string() }),
494
536
  z14.object({ type: z14.literal("session.preview"), id: z14.string(), lines: z14.array(z14.string()) }),
495
537
  /** A session's thread, on request (spec 2026-10-13 §5); then each created or updated item, broadcast. */
@@ -588,7 +630,10 @@ var ClientMessageSchema = z14.discriminatedUnion("type", [
588
630
  machineId: z14.string().optional(),
589
631
  requestId: z14.string().min(1).max(100).optional()
590
632
  }),
591
- z14.object({ type: z14.literal("session.reply"), id: z14.string(), text: z14.string().min(1) }),
633
+ /** `steer`: OMP only, redirect the turn running now instead of queueing the text after it. */
634
+ /** OMP's `ask`: the answers to every question of the dialog `requestId`, in order (`selected` labels, and/or free `text`); `cancel` declines it. */
635
+ z14.object({ type: z14.literal("session.answer"), id: z14.string(), requestId: z14.string().min(1).max(200), answers: z14.array(z14.object({ id: z14.string(), selected: z14.array(z14.string().max(2e3)).max(100), text: z14.string().max(2e4).optional() })).max(50), cancel: z14.boolean().optional() }),
636
+ z14.object({ type: z14.literal("session.reply"), id: z14.string(), text: z14.string().min(1), steer: z14.boolean().optional() }),
592
637
  /** The chat card's instruction (spec 2026-10-12 relay §4): sent now if the session is free, else queued until it is. */
593
638
  z14.object({ type: z14.literal("session.relay"), qid: z14.string().min(1) }),
594
639
  /** Takes the card's queued instruction back out of the queue. */
@@ -605,7 +650,8 @@ var ClientMessageSchema = z14.discriminatedUnion("type", [
605
650
  }),
606
651
  z14.object({ type: z14.literal("session.focus"), id: z14.string() }),
607
652
  z14.object({ type: z14.literal("session.kill"), id: z14.string() }),
608
- z14.object({ type: z14.literal("session.interrupt"), id: z14.string() }),
653
+ /** `restoreQueue` (OMP): also take back what waits in its queue, returned as `session.queue.restored` to put in the draft. */
654
+ z14.object({ type: z14.literal("session.interrupt"), id: z14.string(), restoreQueue: z14.boolean().optional() }),
609
655
  /** Removes a finished (exited) or read-only (hook_only) session from the list; refused for a live launcher session. */
610
656
  z14.object({ type: z14.literal("session.dismiss"), id: z14.string() }),
611
657
  /** Puts a session in the archive: it leaves the live list but is kept. */
@@ -614,6 +660,9 @@ var ClientMessageSchema = z14.discriminatedUnion("type", [
614
660
  z14.object({ type: z14.literal("session.restore"), id: z14.string() }),
615
661
  /** Starts the agent again on an archived session's conversation. */
616
662
  z14.object({ type: z14.literal("session.resume"), id: z14.string() }),
663
+ /** Changes a live session's model and effort (#58, lot 2), `null` for the agent's own default: the daemon restarts the
664
+ * agent on the same conversation with them, at once when the session is free, else at its next free moment. */
665
+ z14.object({ type: z14.literal("session.model"), id: z14.string(), model: ModelIdSchema.nullable(), effort: EffortSchema.nullable() }),
617
666
  /** Deletes a session for good, archived or not. */
618
667
  z14.object({ type: z14.literal("session.delete"), id: z14.string() }),
619
668
  /** The user names the session's topic from the overlay (500 characters at most, the hook's prompt cap); an empty topic hands back to the automatic sources. */
@@ -905,6 +954,7 @@ function readDaemonToken() {
905
954
 
906
955
  export {
907
956
  SessionSchema,
957
+ backendOf,
908
958
  isArchived,
909
959
  MAX_PROPOSALS,
910
960
  proposalRows,
@@ -4,7 +4,7 @@ import {
4
4
  SUMMARY_MAX,
5
5
  parseServerMessage,
6
6
  readDaemonToken
7
- } from "./chunk-VY45MACP.js";
7
+ } from "./chunk-PPRB4XIX.js";
8
8
 
9
9
  // src/client.ts
10
10
  import WebSocket from "ws";
@@ -1,14 +1,14 @@
1
1
  import {
2
2
  DaemonRefusedError,
3
3
  connect
4
- } from "./chunk-XXFUYME7.js";
4
+ } from "./chunk-SVVQ5KPJ.js";
5
5
  import {
6
6
  exitFile,
7
7
  spawnSession
8
- } from "./chunk-X34LPKG7.js";
8
+ } from "./chunk-5XFBRTKO.js";
9
9
  import {
10
10
  PROTOCOL_PORT
11
- } from "./chunk-VY45MACP.js";
11
+ } from "./chunk-PPRB4XIX.js";
12
12
  import {
13
13
  realTmux
14
14
  } from "./chunk-ZEHTNFPV.js";