@paradow/ledge 0.16.1 → 0.17.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/README.md +2 -0
- package/dist/cli/{attach-5H727FCW.js → attach-EYIX7FZN.js} +7 -7
- package/dist/cli/{bridge-LXMOQN6Q.js → bridge-6UHCD76N.js} +2 -2
- package/dist/cli/{chunk-MQICEVKR.js → chunk-36KJUV62.js} +6 -6
- package/dist/cli/chunk-5ZCHFGCV.js +115 -0
- package/dist/cli/{chunk-QJVODYJ5.js → chunk-AQ25HFBP.js} +40 -2
- package/dist/cli/{chunk-R5462SQM.js → chunk-ELLXOV4Q.js} +2 -1
- package/dist/cli/{chunk-BNKPXSUY.js → chunk-JQZHXVIZ.js} +1 -1
- package/dist/cli/{chunk-2H5FJH4Q.js → chunk-LAUNHDTX.js} +2 -2
- package/dist/cli/{chunk-DBSCO3HI.js → chunk-NC5YAEJB.js} +56 -7
- package/dist/cli/{chunk-S4ANPQGA.js → chunk-OVJL2QTN.js} +2 -2
- package/dist/cli/{chunk-TB47EHML.js → chunk-Q7YNLP66.js} +2 -2
- package/dist/cli/{chunk-Q5WWH7FG.js → chunk-QD27W6W5.js} +681 -630
- package/dist/cli/chunk-V2H7UD2N.js +6 -0
- package/dist/cli/{chunk-H36JD4E3.js → chunk-VE4LGNXX.js} +2 -2
- package/dist/cli/{chunk-DEXDXDO4.js → chunk-VVK4I6B5.js} +1 -1
- package/dist/cli/{daemon-DKH63BGV.js → daemon-QEJLAIBT.js} +355 -40
- package/dist/cli/{hook-LY2YTK5U.js → hook-AE5HC6AS.js} +18 -15
- package/dist/cli/index.js +30 -11
- package/dist/cli/{launch-UQEFFPBN.js → launch-B4G5OFBV.js} +7 -7
- package/dist/cli/send-2OZ3YZRV.js +18 -0
- package/dist/cli/{setup-3N2EOJXS.js → setup-2VM4GCDJ.js} +4 -4
- package/dist/cli/{status-ZQSYXILE.js → status-3VJAO6UL.js} +3 -3
- package/dist/cli/task-PMKNYP6M.js +18 -0
- package/dist/cli/tui-DWA2EDC2.js +3561 -0
- package/dist/cli/{uninstall-W2OMKU3B.js → uninstall-AKUWE33D.js} +4 -4
- package/dist/cli/{version-6BMBIXHN.js → version-WOGCIZY3.js} +1 -1
- package/dist/cli/who-37TNDYVA.js +11 -0
- package/package.json +7 -1
- package/dist/cli/chunk-J4ICZEGP.js +0 -6
- package/dist/cli/send-EY63XQXI.js +0 -14
- package/dist/cli/who-VGIMGQNS.js +0 -11
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
NAMES,
|
|
3
3
|
pickName
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-QD27W6W5.js";
|
|
5
5
|
import {
|
|
6
6
|
paths
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-ELLXOV4Q.js";
|
|
8
8
|
|
|
9
9
|
// src/daemon/transcript.ts
|
|
10
10
|
import { appendFileSync, closeSync, fstatSync, openSync, readFileSync, readSync, statSync } from "fs";
|
|
@@ -17,18 +17,24 @@ import {
|
|
|
17
17
|
dialogMismatch,
|
|
18
18
|
flatten,
|
|
19
19
|
formatLedgeMessage,
|
|
20
|
+
formatLedgeNotice,
|
|
21
|
+
formatLedgeTask,
|
|
20
22
|
gitInfo,
|
|
21
23
|
gitSummary,
|
|
24
|
+
inline,
|
|
22
25
|
ledgeBody,
|
|
23
26
|
ledgeMessages,
|
|
27
|
+
ledgeNotice,
|
|
24
28
|
ledgePeer,
|
|
29
|
+
ledgeTask,
|
|
30
|
+
mailId,
|
|
25
31
|
pendingTool,
|
|
26
32
|
readPeers,
|
|
27
33
|
reduce,
|
|
28
34
|
repositoryId,
|
|
29
35
|
shownCommand,
|
|
30
36
|
stripNote
|
|
31
|
-
} from "./chunk-
|
|
37
|
+
} from "./chunk-NC5YAEJB.js";
|
|
32
38
|
import {
|
|
33
39
|
PARENT_AGENT_VARS,
|
|
34
40
|
agentEnv,
|
|
@@ -39,7 +45,7 @@ import {
|
|
|
39
45
|
labelTranscript,
|
|
40
46
|
resumeLabel,
|
|
41
47
|
spawnSession
|
|
42
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-VE4LGNXX.js";
|
|
43
49
|
import {
|
|
44
50
|
CHAT_HISTORY_BUDGET,
|
|
45
51
|
DEFAULT_PROJECTS,
|
|
@@ -99,30 +105,31 @@ import {
|
|
|
99
105
|
proposalRows,
|
|
100
106
|
pushEndpointAllowed,
|
|
101
107
|
requestAuthorized,
|
|
102
|
-
selectDaemonProtocol
|
|
103
|
-
|
|
108
|
+
selectDaemonProtocol,
|
|
109
|
+
taskClosed
|
|
110
|
+
} from "./chunk-QD27W6W5.js";
|
|
104
111
|
import {
|
|
105
112
|
LEDGE_VERSION
|
|
106
|
-
} from "./chunk-
|
|
113
|
+
} from "./chunk-V2H7UD2N.js";
|
|
107
114
|
import {
|
|
108
115
|
SERVICE,
|
|
109
116
|
inDaemonService,
|
|
110
117
|
tmuxServerAtRisk
|
|
111
|
-
} from "./chunk-
|
|
118
|
+
} from "./chunk-OVJL2QTN.js";
|
|
112
119
|
import {
|
|
113
120
|
realTmux,
|
|
114
121
|
runFile
|
|
115
|
-
} from "./chunk-
|
|
122
|
+
} from "./chunk-LAUNHDTX.js";
|
|
116
123
|
import {
|
|
117
124
|
log
|
|
118
|
-
} from "./chunk-
|
|
125
|
+
} from "./chunk-JQZHXVIZ.js";
|
|
119
126
|
import {
|
|
120
127
|
ensureLedgeDir,
|
|
121
128
|
paths
|
|
122
|
-
} from "./chunk-
|
|
129
|
+
} from "./chunk-ELLXOV4Q.js";
|
|
123
130
|
|
|
124
131
|
// src/daemon/index.ts
|
|
125
|
-
import { writeFile as
|
|
132
|
+
import { writeFile as writeFile8, rm as rm6 } from "fs/promises";
|
|
126
133
|
import { join as join17 } from "path";
|
|
127
134
|
|
|
128
135
|
// src/daemon/omp/owner.ts
|
|
@@ -594,6 +601,40 @@ var History = class {
|
|
|
594
601
|
});
|
|
595
602
|
return last;
|
|
596
603
|
}
|
|
604
|
+
const failed = ledgeNotice(ev.text);
|
|
605
|
+
if (failed) {
|
|
606
|
+
this.turns.set(id, turn + 1);
|
|
607
|
+
const peer = this.peerOf(failed.to);
|
|
608
|
+
return this.put(id, {
|
|
609
|
+
kind: "notice",
|
|
610
|
+
id: `notice-${turn + 1}-0`,
|
|
611
|
+
at: ev.at,
|
|
612
|
+
turn: turn + 1,
|
|
613
|
+
notice: "delivery",
|
|
614
|
+
status: "refused",
|
|
615
|
+
count: 1,
|
|
616
|
+
recipients: [failed.to],
|
|
617
|
+
name: failed.to,
|
|
618
|
+
...peer ? { peer } : {},
|
|
619
|
+
...failed.reason ? { reason: failed.reason } : {},
|
|
620
|
+
text: failed.text
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
const task = ledgeTask(ev.text);
|
|
624
|
+
if (task) {
|
|
625
|
+
this.turns.set(id, turn + 1);
|
|
626
|
+
const peer = task.from ? this.peerOf(task.from) : void 0;
|
|
627
|
+
return this.put(id, {
|
|
628
|
+
kind: "notice",
|
|
629
|
+
id: `notice-${turn + 1}-0`,
|
|
630
|
+
at: ev.at,
|
|
631
|
+
turn: turn + 1,
|
|
632
|
+
notice: "other",
|
|
633
|
+
...task.from ? { name: task.from } : {},
|
|
634
|
+
...peer ? { peer } : {},
|
|
635
|
+
text: task.text
|
|
636
|
+
});
|
|
637
|
+
}
|
|
597
638
|
this.turns.set(id, turn + 1);
|
|
598
639
|
return this.put(id, { kind: "prompt", id: `prompt-${turn + 1}`, at: ev.at, turn: turn + 1, text: ev.text });
|
|
599
640
|
}
|
|
@@ -725,15 +766,17 @@ var History = class {
|
|
|
725
766
|
}
|
|
726
767
|
/** Puts a received card, paired with the sender's sent one when the sender is a Ledge session: the last of its
|
|
727
768
|
* messages to this session not read yet, the one whose text starts like the body if any (the hook may have cut
|
|
728
|
-
* either), both read as Ledge types a body (a multi-line one arrives on one line).
|
|
769
|
+
* either), both read as Ledge types a body (a multi-line one arrives on one line). A message the mailbox typed names
|
|
770
|
+
* its id: the sent card of that id is the one, and its text replaces a pointer's. That sent one becomes read, in the sender's thread. */
|
|
729
771
|
receive(id, recv, m) {
|
|
730
772
|
const peer = (m.sender ? this.peerOf(m.sender) : void 0) ?? this.peerOf(m.from) ?? (m.name ? this.peerOf(m.name) : void 0);
|
|
731
773
|
if (!peer) return this.put(id, recv);
|
|
732
774
|
const head = (t) => ledgeBody(t).trim().slice(0, 200);
|
|
733
775
|
const open3 = this.items(peer).filter((i) => i.kind === "sent" && i.peer === id && i.status !== "read");
|
|
734
|
-
const
|
|
776
|
+
const byId = m.id ? open3.find((i) => mailId(i.id) === m.id) : void 0;
|
|
777
|
+
const sent = byId ?? [...open3].reverse().find((i) => head(i.text) === head(recv.text)) ?? open3.at(-1);
|
|
735
778
|
if (sent) this.put(peer, { ...sent, status: "read", received: recv.id });
|
|
736
|
-
return this.put(id, { ...recv, peer, ...sent ? { sent: sent.id } : {} });
|
|
779
|
+
return this.put(id, { ...recv, ...byId ? { text: byId.text } : {}, peer, ...sent ? { sent: sent.id } : {} });
|
|
737
780
|
}
|
|
738
781
|
/** The turn (or the helper) is over: an action still running will not get its result from this side. It shows as
|
|
739
782
|
* unknown, not as waiting forever; a result that arrives later still settles it (callFor finds it by id). */
|
|
@@ -4145,8 +4188,46 @@ function cardOf(threads, qid) {
|
|
|
4145
4188
|
} };
|
|
4146
4189
|
}
|
|
4147
4190
|
|
|
4191
|
+
// src/daemon/journal.ts
|
|
4192
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
4193
|
+
import { writeFile as writeFile7, rename as rename3 } from "fs/promises";
|
|
4194
|
+
function jsonJournal(path, name, read) {
|
|
4195
|
+
let pending;
|
|
4196
|
+
let writing = Promise.resolve();
|
|
4197
|
+
const drain = async () => {
|
|
4198
|
+
while (pending !== void 0) {
|
|
4199
|
+
const state = pending;
|
|
4200
|
+
pending = void 0;
|
|
4201
|
+
const tmp = `${path}.tmp`;
|
|
4202
|
+
try {
|
|
4203
|
+
await writeFile7(tmp, JSON.stringify(state));
|
|
4204
|
+
await rename3(tmp, path);
|
|
4205
|
+
} catch (e) {
|
|
4206
|
+
log(`${name} journal not written: ${e.message}`);
|
|
4207
|
+
}
|
|
4208
|
+
}
|
|
4209
|
+
};
|
|
4210
|
+
return {
|
|
4211
|
+
load() {
|
|
4212
|
+
try {
|
|
4213
|
+
return read(JSON.parse(readFileSync4(path, "utf8")));
|
|
4214
|
+
} catch {
|
|
4215
|
+
return void 0;
|
|
4216
|
+
}
|
|
4217
|
+
},
|
|
4218
|
+
save(state) {
|
|
4219
|
+
const idle = pending === void 0;
|
|
4220
|
+
pending = state;
|
|
4221
|
+
if (idle) writing = writing.then(drain);
|
|
4222
|
+
},
|
|
4223
|
+
flush: () => writing
|
|
4224
|
+
};
|
|
4225
|
+
}
|
|
4226
|
+
|
|
4148
4227
|
// src/daemon/mailbox.ts
|
|
4149
4228
|
var MAX_SEEN = 1e3;
|
|
4229
|
+
var BODY_MAX = 200;
|
|
4230
|
+
var BODY_TTL_MS = 7 * 24 * 3600 * 1e3;
|
|
4150
4231
|
var PASSING = [
|
|
4151
4232
|
/^a decision is already in progress$/,
|
|
4152
4233
|
/^an? \w+ prompt takes accept or deny, not a reply$/,
|
|
@@ -4154,34 +4235,58 @@ var PASSING = [
|
|
|
4154
4235
|
/^free-text answers are not supported yet\b/,
|
|
4155
4236
|
/^the request changed, look again$/
|
|
4156
4237
|
];
|
|
4238
|
+
var GONE = "la session n'existe plus";
|
|
4239
|
+
var ENDED = "la session s'est termin\xE9e avant de le recevoir";
|
|
4240
|
+
var reachable = (s) => !!s && s.source === "launcher" && s.state !== "exited" && !isArchived(s) && (!!s.tmuxSession || backendOf(s) === "omp-rpc");
|
|
4157
4241
|
function createMailbox(d) {
|
|
4158
4242
|
const now = d.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
4159
4243
|
const queue = [];
|
|
4160
4244
|
const seen = /* @__PURE__ */ new Map();
|
|
4245
|
+
const bodies = /* @__PURE__ */ new Map();
|
|
4246
|
+
const save = () => {
|
|
4247
|
+
const oldest = Date.parse(now()) - BODY_TTL_MS;
|
|
4248
|
+
for (const [id, b] of bodies) if (Date.parse(b.at) < oldest || bodies.size > BODY_MAX) bodies.delete(id);
|
|
4249
|
+
d.journal?.save({ queue: [...queue], bodies: [...bodies.values()] });
|
|
4250
|
+
};
|
|
4161
4251
|
const ready = (id) => free(d.store.get(id)) && !d.typing.has(id) && !d.closing?.has(id) && !d.relayQueued(id);
|
|
4252
|
+
function fail2(m, reason) {
|
|
4253
|
+
log(`${m.kind} ${m.rid} to ${m.target} failed: ${reason}`);
|
|
4254
|
+
if (m.kind === "notice") return;
|
|
4255
|
+
d.sent.mark(m.from, m.rid, "failed");
|
|
4256
|
+
const sender = d.store.get(m.from);
|
|
4257
|
+
if (!reachable(sender)) return;
|
|
4258
|
+
notice(sender, formatLedgeNotice({ to: m.to, text: m.text, reason }), `${m.rid}:notice`);
|
|
4259
|
+
}
|
|
4260
|
+
function notice(s, text, rid) {
|
|
4261
|
+
queue.push({ kind: "notice", id: mailId(rid), rid, from: "", sender: { name: "Ledge", tool: s.tool }, target: s.id, to: s.name, text });
|
|
4262
|
+
queueMicrotask(() => kick(s.id));
|
|
4263
|
+
}
|
|
4162
4264
|
async function deliver(m) {
|
|
4163
4265
|
const s = d.store.get(m.target);
|
|
4164
4266
|
if (!s) {
|
|
4165
|
-
|
|
4267
|
+
fail2(m, GONE);
|
|
4268
|
+
save();
|
|
4166
4269
|
queueMicrotask(() => kick(m.target));
|
|
4167
4270
|
return;
|
|
4168
4271
|
}
|
|
4169
4272
|
d.typing.add(m.target);
|
|
4170
4273
|
try {
|
|
4171
4274
|
let refused;
|
|
4275
|
+
const text = m.kind === "notice" ? m.text : formatLedgeMessage({ from: m.sender.name, tool: m.sender.tool, text: m.text, to: s.tool, id: m.id, ...m.summary ? { summary: m.summary } : {} });
|
|
4172
4276
|
try {
|
|
4173
|
-
refused = await d.replyTo(m.target,
|
|
4277
|
+
refused = await d.replyTo(m.target, text);
|
|
4174
4278
|
} catch (e) {
|
|
4175
4279
|
refused = e.message;
|
|
4176
4280
|
}
|
|
4177
4281
|
const live2 = d.store.get(m.target);
|
|
4178
4282
|
if (refused && PASSING.some((re) => re.test(refused)) && live2 && live2.state !== "exited") {
|
|
4179
|
-
log(
|
|
4283
|
+
log(`${m.kind} ${m.rid} to ${m.target} waits: ${refused}`);
|
|
4180
4284
|
queue.unshift(m);
|
|
4181
4285
|
return;
|
|
4182
4286
|
}
|
|
4183
|
-
if (refused)
|
|
4184
|
-
d.sent.mark(m.from, m.rid,
|
|
4287
|
+
if (refused) fail2(m, refused);
|
|
4288
|
+
else if (m.kind === "message") d.sent.mark(m.from, m.rid, "delivered");
|
|
4289
|
+
save();
|
|
4185
4290
|
if (refused) queueMicrotask(() => kick(m.target));
|
|
4186
4291
|
} finally {
|
|
4187
4292
|
d.typing.delete(m.target);
|
|
@@ -4192,18 +4297,22 @@ function createMailbox(d) {
|
|
|
4192
4297
|
const i = queue.findIndex((m2) => m2.target === id);
|
|
4193
4298
|
if (i < 0) return;
|
|
4194
4299
|
const [m] = queue.splice(i, 1);
|
|
4195
|
-
void deliver(m).catch((e) => log(
|
|
4300
|
+
void deliver(m).catch((e) => log(`${m.kind} ${m.rid} failed: ${e.message}`));
|
|
4301
|
+
}
|
|
4302
|
+
function failFor(id, reason) {
|
|
4303
|
+
let changed = false;
|
|
4304
|
+
for (let i = queue.length - 1; i >= 0; i--) if (queue[i].target === id) {
|
|
4305
|
+
const [m] = queue.splice(i, 1);
|
|
4306
|
+
fail2(m, reason);
|
|
4307
|
+
changed = true;
|
|
4308
|
+
}
|
|
4309
|
+
if (changed) save();
|
|
4196
4310
|
}
|
|
4197
4311
|
const unsubscribe = d.store.onChange((c) => {
|
|
4198
4312
|
if (c.type === "recents") return;
|
|
4199
4313
|
const id = c.type === "updated" ? c.session.id : c.id;
|
|
4200
|
-
if (c.type === "removed"
|
|
4201
|
-
|
|
4202
|
-
const [m] = queue.splice(i, 1);
|
|
4203
|
-
d.sent.mark(m.from, m.rid, "failed");
|
|
4204
|
-
}
|
|
4205
|
-
return;
|
|
4206
|
-
}
|
|
4314
|
+
if (c.type === "removed") return failFor(id, GONE);
|
|
4315
|
+
if (c.session.state === "exited") return failFor(id, ENDED);
|
|
4207
4316
|
kick(id);
|
|
4208
4317
|
});
|
|
4209
4318
|
const target = (to) => {
|
|
@@ -4215,6 +4324,30 @@ function createMailbox(d) {
|
|
|
4215
4324
|
if (seen.size > MAX_SEEN) seen.delete(seen.keys().next().value);
|
|
4216
4325
|
return ack;
|
|
4217
4326
|
};
|
|
4327
|
+
const idFor = (rid) => {
|
|
4328
|
+
for (const length of [6, 8, 10, 40]) {
|
|
4329
|
+
const id = mailId(rid, length);
|
|
4330
|
+
const held = bodies.get(id);
|
|
4331
|
+
if (!held || held.rid === rid) return id;
|
|
4332
|
+
}
|
|
4333
|
+
return mailId(rid, 40);
|
|
4334
|
+
};
|
|
4335
|
+
const loaded = d.journal?.load();
|
|
4336
|
+
if (loaded) {
|
|
4337
|
+
for (const b of loaded.bodies) bodies.set(b.id, b);
|
|
4338
|
+
const failed = [];
|
|
4339
|
+
for (const m of loaded.queue) {
|
|
4340
|
+
const t = d.store.get(m.target);
|
|
4341
|
+
if (t && t.state !== "exited") queue.push(m);
|
|
4342
|
+
else failed.push([m, t ? ENDED : GONE]);
|
|
4343
|
+
}
|
|
4344
|
+
for (const [m, reason] of failed) fail2(m, reason);
|
|
4345
|
+
if (failed.length) save();
|
|
4346
|
+
if (queue.length) log(`mailbox: ${queue.length} message(s) still waiting from the last run`);
|
|
4347
|
+
queueMicrotask(() => {
|
|
4348
|
+
for (const id of new Set(queue.map((m) => m.target))) kick(id);
|
|
4349
|
+
});
|
|
4350
|
+
}
|
|
4218
4351
|
return {
|
|
4219
4352
|
submit(m) {
|
|
4220
4353
|
const first = seen.get(m.rid);
|
|
@@ -4226,17 +4359,188 @@ function createMailbox(d) {
|
|
|
4226
4359
|
const sender = d.store.get(m.from);
|
|
4227
4360
|
if (!sender) return refuse("session \xE9mettrice inconnue");
|
|
4228
4361
|
if (t.source === "hook_only" || !t.tmuxSession && backendOf(t) !== "omp-rpc") return refuse(`${t.name} n'a pas \xE9t\xE9 lanc\xE9e par Ledge : impossible de lui \xE9crire`);
|
|
4229
|
-
|
|
4362
|
+
const at = now();
|
|
4363
|
+
const id = idFor(m.rid);
|
|
4364
|
+
d.sent.record(m.from, { rid: m.rid, at, to: t.name, peer: t.id, text: m.text, ...m.summary ? { summary: m.summary } : {} });
|
|
4365
|
+
if (!inline(m.text)) bodies.set(id, { id, rid: m.rid, from: sender.id, fromName: sender.name, target: t.id, to: t.name, at, text: m.text });
|
|
4230
4366
|
const outcome = ready(t.id) ? "accepted" : "queued";
|
|
4231
|
-
queue.push({ rid: m.rid, from: m.from, sender: { name: sender.name, tool: sender.tool }, target: t.id, text: m.text });
|
|
4367
|
+
queue.push({ kind: "message", id, rid: m.rid, from: m.from, sender: { name: sender.name, tool: sender.tool }, target: t.id, to: t.name, text: m.text, ...m.summary ? { summary: m.summary } : {} });
|
|
4368
|
+
save();
|
|
4232
4369
|
const ack = remember(m.rid, { type: "message.ack", rid: m.rid, outcome });
|
|
4233
4370
|
if (outcome === "accepted") kick(t.id);
|
|
4234
4371
|
return ack;
|
|
4235
4372
|
},
|
|
4373
|
+
read({ id, by }) {
|
|
4374
|
+
const b = bodies.get(id);
|
|
4375
|
+
if (!b) return { type: "message.body", id, reason: `aucun message ${id} (seuls les longs messages des 7 derniers jours se relisent)` };
|
|
4376
|
+
if (by !== b.from && by !== b.target) return { type: "message.body", id, reason: `le message ${id} ne t'est pas adress\xE9` };
|
|
4377
|
+
return { type: "message.body", id, from: b.fromName, to: b.to, at: b.at, text: b.text };
|
|
4378
|
+
},
|
|
4379
|
+
notify(target2, text, rid) {
|
|
4380
|
+
const s = d.store.get(target2);
|
|
4381
|
+
if (!reachable(s)) return false;
|
|
4382
|
+
notice(s, text, rid);
|
|
4383
|
+
save();
|
|
4384
|
+
return true;
|
|
4385
|
+
},
|
|
4236
4386
|
kick,
|
|
4237
4387
|
close: unsubscribe
|
|
4238
4388
|
};
|
|
4239
4389
|
}
|
|
4390
|
+
function fileJournal(path) {
|
|
4391
|
+
return jsonJournal(path, "mailbox", (raw) => {
|
|
4392
|
+
const r = raw;
|
|
4393
|
+
return r ? { queue: Array.isArray(r.queue) ? r.queue : [], bodies: Array.isArray(r.bodies) ? r.bodies : [] } : void 0;
|
|
4394
|
+
});
|
|
4395
|
+
}
|
|
4396
|
+
|
|
4397
|
+
// src/daemon/tasks.ts
|
|
4398
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
4399
|
+
var CLOSED_TTL_MS = 7 * 24 * 3600 * 1e3;
|
|
4400
|
+
var CLOSED_MAX = 300;
|
|
4401
|
+
var QUOTE = 200;
|
|
4402
|
+
var quote = (s) => {
|
|
4403
|
+
const flat = s.trim().replace(/\s+/g, " ");
|
|
4404
|
+
return flat.length > QUOTE ? `${flat.slice(0, QUOTE)}\u2026` : flat;
|
|
4405
|
+
};
|
|
4406
|
+
var reachable2 = (s) => !!s && s.source === "launcher" && s.state !== "exited" && !isArchived(s) && (!!s.tmuxSession || backendOf(s) === "omp-rpc");
|
|
4407
|
+
function createTasks(d) {
|
|
4408
|
+
const now = d.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
4409
|
+
const tasks = /* @__PURE__ */ new Map();
|
|
4410
|
+
for (const t of d.journal?.load() ?? []) tasks.set(t.id, t);
|
|
4411
|
+
const save = () => {
|
|
4412
|
+
const oldest = Date.parse(now()) - CLOSED_TTL_MS;
|
|
4413
|
+
const closed = [...tasks.values()].filter(taskClosed);
|
|
4414
|
+
for (const t of closed) if (Date.parse(t.updatedAt) < oldest) tasks.delete(t.id);
|
|
4415
|
+
const left = closed.filter((t) => tasks.has(t.id));
|
|
4416
|
+
for (const t of left.slice(0, Math.max(0, left.length - CLOSED_MAX))) tasks.delete(t.id);
|
|
4417
|
+
d.journal?.save([...tasks.values()]);
|
|
4418
|
+
};
|
|
4419
|
+
const newId = () => {
|
|
4420
|
+
for (; ; ) {
|
|
4421
|
+
const id = `t${randomBytes2(3).toString("hex")}`;
|
|
4422
|
+
if (!tasks.has(id)) return id;
|
|
4423
|
+
}
|
|
4424
|
+
};
|
|
4425
|
+
const target = (to) => {
|
|
4426
|
+
const name = bareName(to);
|
|
4427
|
+
return d.store.all().find((s) => s.name === name && s.state !== "exited" && !isArchived(s));
|
|
4428
|
+
};
|
|
4429
|
+
const put = (t, by, what) => {
|
|
4430
|
+
const at = now();
|
|
4431
|
+
const next2 = { ...t, updatedAt: at, log: [...t.log, { at, by, what }] };
|
|
4432
|
+
tasks.set(t.id, next2);
|
|
4433
|
+
save();
|
|
4434
|
+
return next2;
|
|
4435
|
+
};
|
|
4436
|
+
const tellOwner = (t, from, lead) => d.mailbox.notify(t.owner, formatLedgeTask({
|
|
4437
|
+
id: t.id,
|
|
4438
|
+
from,
|
|
4439
|
+
text: `${lead} \xAB ${t.title} \xBB. Lis-la : ledge task show ${t.id} ; prends-la : ledge task take ${t.id} ; \xE0 la fin : ledge task done ${t.id} "r\xE9sultat" (ou ledge task refuse ${t.id} "raison").`
|
|
4440
|
+
}), `${t.id}:${t.log.length}`);
|
|
4441
|
+
const tellGiver = (t, from, what) => d.mailbox.notify(t.giver, formatLedgeTask({
|
|
4442
|
+
id: t.id,
|
|
4443
|
+
from,
|
|
4444
|
+
text: `T\xE2che \xAB ${t.title} \xBB ${what} (ledge task show ${t.id})`
|
|
4445
|
+
}), `${t.id}:${t.log.length}`);
|
|
4446
|
+
const dropFor = (id) => {
|
|
4447
|
+
for (const t of [...tasks.values()]) {
|
|
4448
|
+
if (t.owner !== id || taskClosed(t)) continue;
|
|
4449
|
+
const dropped = put({ ...t, state: "dropped" }, "Ledge", `${t.ownerName} s'est termin\xE9e sans la finir`);
|
|
4450
|
+
log(`task ${t.id} dropped: ${t.ownerName} ended`);
|
|
4451
|
+
tellGiver(dropped, "Ledge", `abandonn\xE9e : ${t.ownerName} s'est termin\xE9e sans la finir`);
|
|
4452
|
+
}
|
|
4453
|
+
};
|
|
4454
|
+
for (const owner of new Set([...tasks.values()].filter((t) => !taskClosed(t)).map((t) => t.owner))) {
|
|
4455
|
+
const s = d.store.get(owner);
|
|
4456
|
+
if (!s || s.state === "exited") dropFor(owner);
|
|
4457
|
+
}
|
|
4458
|
+
const unsubscribe = d.store.onChange((c) => {
|
|
4459
|
+
if (c.type === "recents") return;
|
|
4460
|
+
if (c.type === "removed") return dropFor(c.id);
|
|
4461
|
+
if (c.session.state === "exited") dropFor(c.session.id);
|
|
4462
|
+
});
|
|
4463
|
+
function run2(cmd) {
|
|
4464
|
+
const refuse = (reason) => ({ type: "task.result", rid: cmd.rid, reason });
|
|
4465
|
+
const ok = (task) => ({ type: "task.result", rid: cmd.rid, task });
|
|
4466
|
+
const me = d.store.get(cmd.by);
|
|
4467
|
+
if (!me) return refuse("session inconnue");
|
|
4468
|
+
if (cmd.op === "list") {
|
|
4469
|
+
const mine = [...tasks.values()].filter((t2) => (t2.owner === me.id || t2.giver === me.id) && (cmd.all || !taskClosed(t2)));
|
|
4470
|
+
return { type: "task.result", rid: cmd.rid, tasks: mine.sort((a, b) => a.at.localeCompare(b.at)) };
|
|
4471
|
+
}
|
|
4472
|
+
if (cmd.op === "give") {
|
|
4473
|
+
if (!cmd.to || !cmd.title) return refuse("il faut un destinataire et un titre");
|
|
4474
|
+
const t2 = target(cmd.to);
|
|
4475
|
+
if (!t2) return refuse(`aucune session Ledge nomm\xE9e ${cmd.to}`);
|
|
4476
|
+
if (t2.id === me.id) return refuse("tu ne peux pas te confier une t\xE2che");
|
|
4477
|
+
if (!reachable2(t2)) return refuse(`${t2.name} n'a pas \xE9t\xE9 lanc\xE9e par Ledge : impossible de lui confier une t\xE2che`);
|
|
4478
|
+
const at = now();
|
|
4479
|
+
const task = {
|
|
4480
|
+
id: newId(),
|
|
4481
|
+
title: cmd.title.trim(),
|
|
4482
|
+
...cmd.text?.trim() ? { text: cmd.text.trim() } : {},
|
|
4483
|
+
giver: me.id,
|
|
4484
|
+
giverName: me.name,
|
|
4485
|
+
owner: t2.id,
|
|
4486
|
+
ownerName: t2.name,
|
|
4487
|
+
state: "open",
|
|
4488
|
+
at,
|
|
4489
|
+
updatedAt: at,
|
|
4490
|
+
log: [{ at, by: me.name, what: `confi\xE9e \xE0 ${t2.name}` }]
|
|
4491
|
+
};
|
|
4492
|
+
tasks.set(task.id, task);
|
|
4493
|
+
save();
|
|
4494
|
+
tellOwner(task, me.name, `T\xE2che de ${me.name} :`);
|
|
4495
|
+
return ok(task);
|
|
4496
|
+
}
|
|
4497
|
+
if (!cmd.task) return refuse("il faut l'id de la t\xE2che");
|
|
4498
|
+
const t = tasks.get(cmd.task);
|
|
4499
|
+
if (!t) return refuse(`aucune t\xE2che ${cmd.task}`);
|
|
4500
|
+
const owner = t.owner === me.id;
|
|
4501
|
+
const giver = t.giver === me.id;
|
|
4502
|
+
if (cmd.op === "show") return owner || giver ? ok(t) : refuse(`la t\xE2che ${t.id} n'est ni \xE0 toi ni de toi`);
|
|
4503
|
+
if (taskClosed(t)) return refuse(`la t\xE2che ${t.id} est d\xE9j\xE0 close (${t.state})`);
|
|
4504
|
+
switch (cmd.op) {
|
|
4505
|
+
case "take":
|
|
4506
|
+
if (!owner) return refuse(`la t\xE2che ${t.id} est \xE0 ${t.ownerName}, pas \xE0 toi`);
|
|
4507
|
+
return ok(t.state === "taken" ? t : put({ ...t, state: "taken" }, me.name, "prise"));
|
|
4508
|
+
case "done": {
|
|
4509
|
+
if (!owner) return refuse(`seule la session qui la tient (${t.ownerName}) peut la terminer`);
|
|
4510
|
+
const result = cmd.text?.trim();
|
|
4511
|
+
const done = put({ ...t, state: "done", ...result ? { result } : {} }, me.name, result ? `termin\xE9e : ${result}` : "termin\xE9e");
|
|
4512
|
+
tellGiver(done, me.name, `termin\xE9e par ${me.name}${result ? ` : ${quote(result)}` : ""}`);
|
|
4513
|
+
return ok(done);
|
|
4514
|
+
}
|
|
4515
|
+
case "refuse": {
|
|
4516
|
+
if (!owner && !giver) return refuse(`la t\xE2che ${t.id} n'est ni \xE0 toi ni de toi`);
|
|
4517
|
+
const reason = cmd.text?.trim();
|
|
4518
|
+
if (!reason) return refuse("il faut une raison");
|
|
4519
|
+
const refused = put({ ...t, state: "refused", result: reason }, me.name, `${owner ? "refus\xE9e" : "annul\xE9e"} : ${reason}`);
|
|
4520
|
+
if (owner) tellGiver(refused, me.name, `refus\xE9e par ${me.name} : ${quote(reason)}`);
|
|
4521
|
+
if (giver && !owner) d.mailbox.notify(t.owner, formatLedgeTask({ id: t.id, from: me.name, text: `T\xE2che \xAB ${t.title} \xBB annul\xE9e par ${me.name} : ${quote(reason)}. Arr\xEAte d'y travailler.` }), `${t.id}:${refused.log.length}`);
|
|
4522
|
+
return ok(refused);
|
|
4523
|
+
}
|
|
4524
|
+
case "pass": {
|
|
4525
|
+
if (!owner) return refuse(`seule la session qui la tient (${t.ownerName}) peut la passer`);
|
|
4526
|
+
if (!cmd.to) return refuse("il faut le nom de la session \xE0 qui la passer");
|
|
4527
|
+
const next2 = target(cmd.to);
|
|
4528
|
+
if (!next2) return refuse(`aucune session Ledge nomm\xE9e ${cmd.to}`);
|
|
4529
|
+
if (next2.id === me.id) return refuse("elle est d\xE9j\xE0 \xE0 toi");
|
|
4530
|
+
if (!reachable2(next2)) return refuse(`${next2.name} n'a pas \xE9t\xE9 lanc\xE9e par Ledge : impossible de lui passer une t\xE2che`);
|
|
4531
|
+
const note = cmd.text?.trim();
|
|
4532
|
+
const passed = put({ ...t, owner: next2.id, ownerName: next2.name, state: "open" }, me.name, `pass\xE9e \xE0 ${next2.name}${note ? ` : ${note}` : ""}`);
|
|
4533
|
+
tellOwner(passed, me.name, `T\xE2che pass\xE9e par ${me.name}${note ? ` (${quote(note)})` : ""}, confi\xE9e par ${t.giverName} :`);
|
|
4534
|
+
return ok(passed);
|
|
4535
|
+
}
|
|
4536
|
+
}
|
|
4537
|
+
return refuse("op\xE9ration inconnue");
|
|
4538
|
+
}
|
|
4539
|
+
return { run: run2, close: unsubscribe };
|
|
4540
|
+
}
|
|
4541
|
+
function tasksJournal(path) {
|
|
4542
|
+
return jsonJournal(path, "tasks", (raw) => Array.isArray(raw) ? raw : void 0);
|
|
4543
|
+
}
|
|
4240
4544
|
|
|
4241
4545
|
// src/chat/routing.ts
|
|
4242
4546
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
@@ -4947,7 +5251,7 @@ function capabilities() {
|
|
|
4947
5251
|
}
|
|
4948
5252
|
|
|
4949
5253
|
// src/mobile/challenges.ts
|
|
4950
|
-
import { randomBytes as
|
|
5254
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
4951
5255
|
var CHALLENGE_TTL_MS = 6e4;
|
|
4952
5256
|
var MAX_OUTSTANDING = 1e3;
|
|
4953
5257
|
function createChallengeStore(opts = {}) {
|
|
@@ -4966,7 +5270,7 @@ function createChallengeStore(opts = {}) {
|
|
|
4966
5270
|
const slot2 = slotOf(b);
|
|
4967
5271
|
const previous = slots.get(slot2);
|
|
4968
5272
|
if (previous !== void 0) pending.delete(previous);
|
|
4969
|
-
const challenge =
|
|
5273
|
+
const challenge = randomBytes3(32).toString("base64url");
|
|
4970
5274
|
pending.set(challenge, { binding: { deviceId: b.deviceId, purpose: b.purpose, subject: b.subject }, issuedAt: now(), slot: slot2 });
|
|
4971
5275
|
slots.set(slot2, challenge);
|
|
4972
5276
|
while (pending.size > max) {
|
|
@@ -5586,13 +5890,13 @@ function createPhoneGateway(deps) {
|
|
|
5586
5890
|
}
|
|
5587
5891
|
|
|
5588
5892
|
// src/mobile/push/encrypt.ts
|
|
5589
|
-
import { createCipheriv, createECDH, hkdfSync, randomBytes as
|
|
5893
|
+
import { createCipheriv, createECDH, hkdfSync, randomBytes as randomBytes4 } from "crypto";
|
|
5590
5894
|
var RS = 4096;
|
|
5591
5895
|
var expand = (ikm, salt, info, len) => Buffer.from(hkdfSync("sha256", ikm, salt, Buffer.from(info, "latin1"), len));
|
|
5592
5896
|
function encryptPush(plain, sub, opts = {}) {
|
|
5593
5897
|
const uaPublic = Buffer.from(sub.p256dh, "base64url");
|
|
5594
5898
|
const authSecret = Buffer.from(sub.auth, "base64url");
|
|
5595
|
-
const salt = Buffer.from(opts.salt ??
|
|
5899
|
+
const salt = Buffer.from(opts.salt ?? randomBytes4(16));
|
|
5596
5900
|
if (salt.length !== 16) throw new Error("salt must be 16 octets");
|
|
5597
5901
|
if (uaPublic.length !== 65 || uaPublic[0] !== 4) throw new Error("p256dh is not an uncompressed P-256 point");
|
|
5598
5902
|
const eph = createECDH("prime256v1");
|
|
@@ -5628,7 +5932,7 @@ function encryptPush(plain, sub, opts = {}) {
|
|
|
5628
5932
|
// src/mobile/push/triggers.ts
|
|
5629
5933
|
var TAG_MAX = 32;
|
|
5630
5934
|
var TITLE_MAX = 80;
|
|
5631
|
-
var
|
|
5935
|
+
var BODY_MAX2 = 120;
|
|
5632
5936
|
var GENERIC_REASONS = /* @__PURE__ */ new Set(["Allow the requested shell command?"]);
|
|
5633
5937
|
var safeTag = (s) => s.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, TAG_MAX);
|
|
5634
5938
|
function cut(s, n) {
|
|
@@ -5652,11 +5956,11 @@ function pushFor(before, after, all) {
|
|
|
5652
5956
|
const p = after.pending;
|
|
5653
5957
|
if (p && (!before?.pending || pendingKey(p) !== pendingKey(before.pending))) {
|
|
5654
5958
|
const url = `/#/request/${encodeURIComponent(after.id)}/${encodeURIComponent(pendingKey(p))}`;
|
|
5655
|
-
if (p.kind === "question") return { v: 1, kind: "question", title: cut(`${after.name} a une question`, TITLE_MAX), body: cut(p.text.trim(),
|
|
5656
|
-
return { v: 1, kind: "request", title: cut(`${after.name} attend votre accord`, TITLE_MAX), body: cut(requestBody(after),
|
|
5959
|
+
if (p.kind === "question") return { v: 1, kind: "question", title: cut(`${after.name} a une question`, TITLE_MAX), body: cut(p.text.trim(), BODY_MAX2), url, badge, tag };
|
|
5960
|
+
return { v: 1, kind: "request", title: cut(`${after.name} attend votre accord`, TITLE_MAX), body: cut(requestBody(after), BODY_MAX2), url, badge, tag };
|
|
5657
5961
|
}
|
|
5658
5962
|
if (before?.state === "working" && after.state === "idle" && !p) {
|
|
5659
|
-
return { v: 1, kind: "finished", title: cut(`${after.name} a termin\xE9`, TITLE_MAX), body: cut((after.lastReply?.text ?? "").trim(),
|
|
5963
|
+
return { v: 1, kind: "finished", title: cut(`${after.name} a termin\xE9`, TITLE_MAX), body: cut((after.lastReply?.text ?? "").trim(), BODY_MAX2), url: `/#/thread/${encodeURIComponent(after.id)}`, badge, tag };
|
|
5660
5964
|
}
|
|
5661
5965
|
return null;
|
|
5662
5966
|
}
|
|
@@ -5906,12 +6210,14 @@ async function startServer(opts) {
|
|
|
5906
6210
|
typing,
|
|
5907
6211
|
closing,
|
|
5908
6212
|
now,
|
|
6213
|
+
...opts.mail ? { journal: opts.mail } : {},
|
|
5909
6214
|
relayQueued: (id) => cardThreads.queuedRelays().some((q) => q.relay.id === id),
|
|
5910
6215
|
sent: { record: (id, m) => {
|
|
5911
6216
|
history?.recordSent(id, m);
|
|
5912
6217
|
}, mark: (id, rid, status) => history?.markSent(id, rid, status) }
|
|
5913
6218
|
});
|
|
5914
6219
|
const relay = createRelay({ threads: cardThreads, store, replyTo: replyText, now, typing, closing, next: (id) => mailbox.kick(id) });
|
|
6220
|
+
const tasks = createTasks({ store, mailbox, now, ...opts.tasks ? { journal: opts.tasks } : {} });
|
|
5915
6221
|
const previews = /* @__PURE__ */ new Map();
|
|
5916
6222
|
const titles = /* @__PURE__ */ new Map();
|
|
5917
6223
|
const labelled = /* @__PURE__ */ new Map();
|
|
@@ -6583,6 +6889,10 @@ async function startServer(opts) {
|
|
|
6583
6889
|
// An agent's message to another session: acked at once, typed when the target is free.
|
|
6584
6890
|
case "session.message":
|
|
6585
6891
|
return reply(mailbox.submit(msg));
|
|
6892
|
+
case "message.read":
|
|
6893
|
+
return reply(mailbox.read(msg));
|
|
6894
|
+
case "task.cmd":
|
|
6895
|
+
return reply(tasks.run(msg));
|
|
6586
6896
|
// A chat card's instruction: a refusal reaches the requester as an `error`, the card's state everyone through chat.history.
|
|
6587
6897
|
case "session.relay":
|
|
6588
6898
|
return relay.request(msg.qid);
|
|
@@ -6777,6 +7087,7 @@ async function startServer(opts) {
|
|
|
6777
7087
|
gateway?.stop();
|
|
6778
7088
|
relay.close();
|
|
6779
7089
|
mailbox.close();
|
|
7090
|
+
tasks.close();
|
|
6780
7091
|
void omp.closeAll();
|
|
6781
7092
|
for (const c of wss.clients) c.terminate();
|
|
6782
7093
|
for (const socket of sockets) socket.destroy();
|
|
@@ -6878,7 +7189,7 @@ ${output}` } : { code: r.code, output };
|
|
|
6878
7189
|
}
|
|
6879
7190
|
|
|
6880
7191
|
// src/mobile/files.ts
|
|
6881
|
-
import { readFileSync as
|
|
7192
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, renameSync as renameSync4, chmodSync, unlinkSync } from "fs";
|
|
6882
7193
|
import { z } from "zod";
|
|
6883
7194
|
|
|
6884
7195
|
// ../seal/src/errors.ts
|
|
@@ -7162,7 +7473,7 @@ function writePrivate(file, content) {
|
|
|
7162
7473
|
}
|
|
7163
7474
|
function readText(file, name) {
|
|
7164
7475
|
try {
|
|
7165
|
-
return
|
|
7476
|
+
return readFileSync5(file, "utf8");
|
|
7166
7477
|
} catch (e) {
|
|
7167
7478
|
if (e.code === "ENOENT") return null;
|
|
7168
7479
|
throw new Error(`${name} cannot be read (${e.code ?? "error"})`);
|
|
@@ -8288,10 +8599,12 @@ async function startDaemon(opts = {}) {
|
|
|
8288
8599
|
log(`mobile access not available: ${e.message}`);
|
|
8289
8600
|
mobileUnavailable = e.message.slice(0, 200);
|
|
8290
8601
|
}
|
|
8291
|
-
const
|
|
8602
|
+
const mail = fileJournal(paths().mail);
|
|
8603
|
+
const taskJournal = tasksJournal(paths().tasks);
|
|
8604
|
+
const server = await startServer({ store, tmux, port: opts.port, daemon, updater, history, mobile, mobileUnavailable, mail, tasks: taskJournal });
|
|
8292
8605
|
ompLive = () => server.omp.liveCount();
|
|
8293
8606
|
const poller = startPoller({ store, tmux, broadcastPreview: server.preview, labelExited: server.labelExited });
|
|
8294
|
-
await
|
|
8607
|
+
await writeFile8(paths().pid, String(process.pid));
|
|
8295
8608
|
log(`daemon started on ${server.port} with ${store.all().length} sessions`);
|
|
8296
8609
|
let stopping = null;
|
|
8297
8610
|
const stop = () => stopping ??= (async () => {
|
|
@@ -8302,6 +8615,8 @@ async function startDaemon(opts = {}) {
|
|
|
8302
8615
|
await mobile?.stop();
|
|
8303
8616
|
await store.save();
|
|
8304
8617
|
await history.flush();
|
|
8618
|
+
await mail.flush();
|
|
8619
|
+
await taskJournal.flush();
|
|
8305
8620
|
await rm6(paths().pid, { force: true });
|
|
8306
8621
|
log("daemon stopped");
|
|
8307
8622
|
})();
|