@lelouchhe/webagent 0.8.0 → 0.10.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 (56) hide show
  1. package/README.md +43 -15
  2. package/config.toml +7 -27
  3. package/dist/index.html +21 -5
  4. package/dist/js/app.INIQQEGD.js +5 -0
  5. package/dist/js/chunk.3CLGCUHW.js +1 -0
  6. package/dist/js/{chunk.CT5WBNGZ.js → chunk.7WADDFJZ.js} +50 -49
  7. package/dist/js/chunk.AOTG3PL7.js +20 -0
  8. package/dist/js/{login.2WA6DTGM.js → login.WMURU4NI.js} +1 -1
  9. package/dist/js/viewer.RHZMFYWJ.js +1 -0
  10. package/dist/login.html +2 -2
  11. package/dist/share-viewer.html +6 -6
  12. package/dist/{styles.00etlpgs.css → styles.01aj0l37.css} +186 -4
  13. package/dist/sw.js +6 -6
  14. package/lib/agent-key.js +6 -0
  15. package/lib/attachment-dispatch.js +60 -31
  16. package/lib/attachment-interceptor.js +7 -7
  17. package/lib/attachment-labels.js +1 -1
  18. package/lib/attachments.js +69 -7
  19. package/lib/auth-middleware.js +11 -4
  20. package/lib/auth.js +2 -2
  21. package/lib/bridge.js +209 -90
  22. package/lib/client-registry.js +12 -12
  23. package/lib/config.js +2 -31
  24. package/lib/event-handler.js +166 -85
  25. package/lib/files/limits.js +15 -0
  26. package/lib/files/paths.js +155 -0
  27. package/lib/files/routes.js +232 -0
  28. package/lib/home-path.js +35 -0
  29. package/lib/http-status.js +1 -0
  30. package/lib/mcp/capability.js +74 -0
  31. package/lib/mcp/server.js +148 -0
  32. package/lib/mcp/task-history.js +245 -0
  33. package/lib/mcp/task-host.js +253 -0
  34. package/lib/mcp/tools.js +168 -0
  35. package/lib/mode-bucket.js +1 -1
  36. package/lib/push-service.js +33 -35
  37. package/lib/routes.js +1022 -475
  38. package/lib/server.js +84 -34
  39. package/lib/share/routes.js +97 -85
  40. package/lib/shared/task-reference.js +20 -0
  41. package/lib/sse-manager.js +8 -8
  42. package/lib/store.js +992 -284
  43. package/lib/task-collaboration.js +15 -0
  44. package/lib/task-manager.js +1409 -0
  45. package/lib/task-path.js +131 -0
  46. package/lib/{session-state.js → task-state.js} +90 -38
  47. package/lib/task-tree-lock.js +74 -0
  48. package/lib/{sessions-anchor.js → tasks-anchor.js} +8 -7
  49. package/lib/tokens.js +1 -1
  50. package/lib/types.js +2 -2
  51. package/package.json +8 -1
  52. package/dist/js/app.XBFXH37R.js +0 -2
  53. package/dist/js/chunk.UMQMOGWO.js +0 -1
  54. package/dist/js/viewer.CVWXSKJM.js +0 -1
  55. package/lib/session-manager.js +0 -613
  56. package/lib/title-service.js +0 -95
@@ -0,0 +1,131 @@
1
+ /** Server-side syntax primitives for task-target commands. */
2
+ /** An invalid command head is rejected rather than being guessed as ordinary input. */
3
+ export class TaskPathParseError extends Error {
4
+ constructor(message) {
5
+ super(message);
6
+ this.name = "TaskPathParseError";
7
+ }
8
+ }
9
+ function isWhitespace(char) {
10
+ return /\s/u.test(char);
11
+ }
12
+ function skipWhitespace(source, offset) {
13
+ let cursor = offset;
14
+ while (cursor < source.length && isWhitespace(source[cursor]))
15
+ cursor++;
16
+ return cursor;
17
+ }
18
+ /**
19
+ * Parse one deliberately small shell-style word.
20
+ *
21
+ * Quotes and backslash escapes exist only as input syntax. This is not a shell:
22
+ * variables, command substitution, globbing, comments, and operators are all
23
+ * ordinary characters.
24
+ */
25
+ function parseShellWord(source, start) {
26
+ let cursor = start;
27
+ let value = "";
28
+ let consumed = false;
29
+ while (cursor < source.length && !isWhitespace(source[cursor])) {
30
+ const char = source[cursor];
31
+ if (char === "\\") {
32
+ if (cursor + 1 >= source.length) {
33
+ throw new TaskPathParseError("Target ends with an incomplete escape");
34
+ }
35
+ value += source[cursor + 1];
36
+ cursor += 2;
37
+ consumed = true;
38
+ continue;
39
+ }
40
+ if (char === "'" || char === '"') {
41
+ const quote = char;
42
+ cursor++;
43
+ consumed = true;
44
+ let closed = false;
45
+ while (cursor < source.length) {
46
+ const quoted = source[cursor];
47
+ if (quoted === quote) {
48
+ cursor++;
49
+ closed = true;
50
+ break;
51
+ }
52
+ if (quote === '"' && quoted === "\\") {
53
+ if (cursor + 1 >= source.length) {
54
+ throw new TaskPathParseError("Target ends with an incomplete escape inside double quotes");
55
+ }
56
+ value += source[cursor + 1];
57
+ cursor += 2;
58
+ continue;
59
+ }
60
+ value += quoted;
61
+ cursor++;
62
+ }
63
+ if (!closed) {
64
+ throw new TaskPathParseError("Target contains an unterminated quote");
65
+ }
66
+ continue;
67
+ }
68
+ value += char;
69
+ cursor++;
70
+ consumed = true;
71
+ }
72
+ if (!consumed || value.length === 0) {
73
+ throw new TaskPathParseError("Task command requires a non-empty target");
74
+ }
75
+ return { value, end: cursor };
76
+ }
77
+ /**
78
+ * Decode a slash-separated path without resolving it against a task tree or
79
+ * filesystem. Consecutive separators are normalized; `.` and `..` remain so
80
+ * the receiving resolver can enforce its own root and visibility policy.
81
+ */
82
+ export function parseTaskPath(target) {
83
+ // An empty target is a bare `+`/`@`: the caller lists its default scope
84
+ // instead of resolving a path.
85
+ if (!target)
86
+ return { absolute: false, segments: [] };
87
+ const absolute = target.startsWith("/");
88
+ const segments = target.split("/").filter(Boolean);
89
+ return {
90
+ absolute,
91
+ segments,
92
+ ...(target.endsWith("/") ? { trailingSlash: true } : {}),
93
+ };
94
+ }
95
+ /**
96
+ * Parse the command marker and its target word. Policy and path resolution are
97
+ * intentionally outside this module; callers retain the raw remainder as the
98
+ * eventual message body or creation brief.
99
+ */
100
+ export function parseTaskCommand(source) {
101
+ const markerStart = skipWhitespace(source, 0);
102
+ let marker;
103
+ let targetStart;
104
+ if (source.startsWith("@!", markerStart)) {
105
+ marker = "@!";
106
+ targetStart = markerStart + 2;
107
+ }
108
+ else if (source[markerStart] === "+" || source[markerStart] === "@") {
109
+ marker = source[markerStart];
110
+ targetStart = markerStart + 1;
111
+ }
112
+ else {
113
+ throw new TaskPathParseError("Task command must start with +, @, or @!");
114
+ }
115
+ const start = skipWhitespace(source, targetStart);
116
+ if (start >= source.length) {
117
+ return {
118
+ marker,
119
+ target: "",
120
+ path: { absolute: false, segments: [] },
121
+ remainder: source.slice(targetStart),
122
+ };
123
+ }
124
+ const word = parseShellWord(source, start);
125
+ return {
126
+ marker,
127
+ target: word.value,
128
+ path: parseTaskPath(word.value),
129
+ remainder: source.slice(word.end),
130
+ };
131
+ }
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Per-session runtime state: single source of truth for "what state is this
3
- * session in right now" (busy / streaming / pending permissions / plan).
2
+ * Per-task runtime state: single source of truth for "what state is this
3
+ * task in right now" (busy / streaming / pending permissions / plan).
4
4
  *
5
5
  * The frontend fetches a full snapshot on connect / reconnect / after long
6
6
  * backgrounding, then applies incremental `state_patch` SSE events. This
@@ -17,6 +17,7 @@ function defaultState() {
17
17
  pendingPermissions: [],
18
18
  streaming: { assistant: false, thinking: false },
19
19
  plan: null,
20
+ contextUsage: null,
20
21
  },
21
22
  };
22
23
  }
@@ -56,6 +57,14 @@ function plansEqual(a, b) {
56
57
  return false;
57
58
  return a.every((entry, index) => entry.status === b[index].status && entry.content === b[index].content);
58
59
  }
60
+ function contextUsageEqual(a, b) {
61
+ if (a === null || b === null)
62
+ return a === b;
63
+ return (a.used === b.used &&
64
+ a.size === b.size &&
65
+ (a.cost?.amount ?? null) === (b.cost?.amount ?? null) &&
66
+ (a.cost?.currency ?? null) === (b.cost?.currency ?? null));
67
+ }
59
68
  /** True when the patch would change the current runtime state. */
60
69
  function hasRuntimeChanges(current, patch) {
61
70
  if (!patch)
@@ -68,6 +77,9 @@ function hasRuntimeChanges(current, patch) {
68
77
  return true;
69
78
  if ("plan" in patch && !plansEqual(current.plan, patch.plan ?? null))
70
79
  return true;
80
+ if ("contextUsage" in patch &&
81
+ !contextUsageEqual(current.contextUsage, patch.contextUsage ?? null))
82
+ return true;
71
83
  if ("streaming" in patch && patch.streaming) {
72
84
  const s = patch.streaming;
73
85
  if (s.assistant !== undefined &&
@@ -78,31 +90,31 @@ function hasRuntimeChanges(current, patch) {
78
90
  }
79
91
  return false;
80
92
  }
81
- export class SessionStateManager {
93
+ export class TaskStateManager {
82
94
  states = new Map();
83
95
  listeners = new Set();
84
96
  cancelTimers = new Map();
85
97
  /** Get current state (creates default entry on first access). */
86
- getState(sessionId) {
87
- let s = this.states.get(sessionId);
98
+ getState(taskId) {
99
+ let s = this.states.get(taskId);
88
100
  if (!s) {
89
101
  s = defaultState();
90
- this.states.set(sessionId, s);
102
+ this.states.set(taskId, s);
91
103
  }
92
104
  return s;
93
105
  }
94
- /** Read streaming state without creating runtime state for an unseen session. */
95
- peekStreaming(sessionId) {
96
- const streaming = this.states.get(sessionId)?.runtime.streaming;
106
+ /** Read streaming state without creating runtime state for an unseen task. */
107
+ peekStreaming(taskId) {
108
+ const streaming = this.states.get(taskId)?.runtime.streaming;
97
109
  return streaming ? { ...streaming } : { assistant: false, thinking: false };
98
110
  }
99
111
  /**
100
- * Merge a patch into the session's runtime state. Bumps seq and notifies
112
+ * Merge a patch into the task's runtime state. Bumps seq and notifies
101
113
  * listeners only when the patch actually changes something (no-op patches
102
114
  * are dropped silently).
103
115
  */
104
- patch(sessionId, patch) {
105
- const state = this.getState(sessionId);
116
+ patch(taskId, patch) {
117
+ const state = this.getState(taskId);
106
118
  const runtimeChanged = hasRuntimeChanges(state.runtime, patch.runtime);
107
119
  if (!runtimeChanged)
108
120
  return;
@@ -119,6 +131,15 @@ export class SessionStateManager {
119
131
  state.runtime.plan =
120
132
  patch.runtime.plan?.map((entry) => ({ ...entry })) ?? null;
121
133
  }
134
+ if ("contextUsage" in patch.runtime) {
135
+ const usage = patch.runtime.contextUsage;
136
+ state.runtime.contextUsage = usage
137
+ ? {
138
+ ...usage,
139
+ ...(usage.cost ? { cost: { ...usage.cost } } : {}),
140
+ }
141
+ : null;
142
+ }
122
143
  if ("streaming" in patch.runtime && patch.runtime.streaming) {
123
144
  if (patch.runtime.streaming.assistant !== undefined) {
124
145
  state.runtime.streaming.assistant = patch.runtime.streaming.assistant;
@@ -131,7 +152,7 @@ export class SessionStateManager {
131
152
  state.seq += 1;
132
153
  const event = {
133
154
  type: "state_patch",
134
- sessionId,
155
+ taskId,
135
156
  seq: state.seq,
136
157
  patch,
137
158
  };
@@ -145,29 +166,60 @@ export class SessionStateManager {
145
166
  this.listeners.delete(cb);
146
167
  };
147
168
  }
148
- /** Clear all state for a session (call from SessionManager.deleteSession). */
149
- delete(sessionId) {
150
- this.states.delete(sessionId);
151
- const t = this.cancelTimers.get(sessionId);
169
+ /** Clear all state for a task (call from TaskManager.deleteTask). */
170
+ delete(taskId) {
171
+ this.states.delete(taskId);
172
+ const t = this.cancelTimers.get(taskId);
152
173
  if (t) {
153
174
  clearTimeout(t);
154
- this.cancelTimers.delete(sessionId);
175
+ this.cancelTimers.delete(taskId);
155
176
  }
156
177
  }
157
- /** Clear current plans for every known session (used on bridge reload). */
178
+ /**
179
+ * Reset a live task's runtime state to defaults without restarting the
180
+ * seq ledger. Used when rotating a task's ACP execution in place (clear):
181
+ * the WebAgent task identity survives, so clients that validate
182
+ * incremental `state_patch` events against their own lastStateSeq must keep
183
+ * seeing a monotonic seq. Hard-deleting the entry here would restart the
184
+ * server seq at 0; every post-rotation snapshot would then look
185
+ * "superseded" to the client and be dropped, leaving it permanently
186
+ * desynced (stuck busy). Broadcasts one patch when anything actually
187
+ * changed, mirroring the explicit reset patch used by the compaction path.
188
+ */
189
+ reset(taskId) {
190
+ this.clearCancelSafety(taskId);
191
+ this.patch(taskId, {
192
+ runtime: {
193
+ busy: null,
194
+ pendingPermissions: [],
195
+ streaming: { assistant: false, thinking: false },
196
+ plan: null,
197
+ contextUsage: null,
198
+ },
199
+ });
200
+ }
201
+ /** Clear current plans for every known task (used on bridge reload). */
158
202
  clearPlans() {
159
- for (const [sessionId, state] of this.states) {
203
+ for (const [taskId, state] of this.states) {
160
204
  if (state.runtime.plan !== null) {
161
- this.patch(sessionId, { runtime: { plan: null } });
205
+ this.patch(taskId, { runtime: { plan: null } });
206
+ }
207
+ }
208
+ }
209
+ /** Clear context usage for every known task on bridge teardown. */
210
+ clearContextUsage() {
211
+ for (const [taskId, state] of this.states) {
212
+ if (state.runtime.contextUsage !== null) {
213
+ this.patch(taskId, { runtime: { contextUsage: null } });
162
214
  }
163
215
  }
164
216
  }
165
- /** Clear active stream markers for every known session on bridge teardown. */
217
+ /** Clear active stream markers for every known task on bridge teardown. */
166
218
  clearStreaming() {
167
- for (const [sessionId, state] of this.states) {
219
+ for (const [taskId, state] of this.states) {
168
220
  if (state.runtime.streaming.assistant ||
169
221
  state.runtime.streaming.thinking) {
170
- this.patch(sessionId, {
222
+ this.patch(taskId, {
171
223
  runtime: {
172
224
  streaming: { assistant: false, thinking: false },
173
225
  },
@@ -178,23 +230,23 @@ export class SessionStateManager {
178
230
  /**
179
231
  * Backend acknowledgement timer for cancel: if the same agent prompt is
180
232
  * still pending after `timeoutMs`, mark the request unconfirmed.
181
- * A second arm on the same session replaces the existing timer.
233
+ * A second arm on the same task replaces the existing timer.
182
234
  */
183
- armCancelSafety(sessionId, timeoutMs) {
235
+ armCancelSafety(taskId, timeoutMs) {
184
236
  if (timeoutMs <= 0)
185
237
  return;
186
- const existing = this.cancelTimers.get(sessionId);
238
+ const existing = this.cancelTimers.get(taskId);
187
239
  if (existing)
188
240
  clearTimeout(existing);
189
241
  const t = setTimeout(() => {
190
- this.cancelTimers.delete(sessionId);
191
- const busy = this.getState(sessionId).runtime.busy;
242
+ this.cancelTimers.delete(taskId);
243
+ const busy = this.getState(taskId).runtime.busy;
192
244
  if (busy?.kind === "agent" && busy.cancelStatus === "requested") {
193
245
  clog.warn("agent did not acknowledge", {
194
- sessionId: sessionId.slice(0, 8),
246
+ taskId: taskId.slice(0, 8),
195
247
  promptId: busy.promptId,
196
248
  });
197
- this.patch(sessionId, {
249
+ this.patch(taskId, {
198
250
  runtime: {
199
251
  busy: { ...busy, cancelStatus: "unconfirmed" },
200
252
  },
@@ -203,23 +255,23 @@ export class SessionStateManager {
203
255
  }, timeoutMs);
204
256
  if (typeof t === "object" && "unref" in t)
205
257
  t.unref();
206
- this.cancelTimers.set(sessionId, t);
258
+ this.cancelTimers.set(taskId, t);
207
259
  }
208
260
  /** Mark that a cancel notification was sent for the active agent prompt. */
209
- markCancelRequested(sessionId) {
210
- const busy = this.getState(sessionId).runtime.busy;
261
+ markCancelRequested(taskId) {
262
+ const busy = this.getState(taskId).runtime.busy;
211
263
  if (busy?.kind !== "agent")
212
264
  return;
213
- this.patch(sessionId, {
265
+ this.patch(taskId, {
214
266
  runtime: { busy: { ...busy, cancelStatus: "requested" } },
215
267
  });
216
268
  }
217
269
  /** Cancel the safety net timer (e.g. when prompt_done arrives naturally). */
218
- clearCancelSafety(sessionId) {
219
- const t = this.cancelTimers.get(sessionId);
270
+ clearCancelSafety(taskId) {
271
+ const t = this.cancelTimers.get(taskId);
220
272
  if (t) {
221
273
  clearTimeout(t);
222
- this.cancelTimers.delete(sessionId);
274
+ this.cancelTimers.delete(taskId);
223
275
  }
224
276
  }
225
277
  }
@@ -0,0 +1,74 @@
1
+ function normalize(spec) {
2
+ const exclusive = new Set(spec.exclusive ?? []);
3
+ const shared = new Set((spec.shared ?? []).filter((key) => !exclusive.has(key)));
4
+ return { shared, exclusive };
5
+ }
6
+ function conflicts(a, b) {
7
+ for (const key of a.exclusive) {
8
+ if (b.exclusive.has(key) || b.shared.has(key))
9
+ return true;
10
+ }
11
+ for (const key of a.shared) {
12
+ if (b.exclusive.has(key))
13
+ return true;
14
+ }
15
+ return false;
16
+ }
17
+ /**
18
+ * Small in-process hierarchical lock scheduler for one task tree.
19
+ *
20
+ * Requests are granted atomically, so callers never hold part of a lineage
21
+ * while waiting for another node. Compatible sibling requests can be active
22
+ * together; an earlier conflicting request keeps later requests from
23
+ * overtaking it. Releases are idempotent so failure paths can clean up safely.
24
+ */
25
+ export class TaskTreeLock {
26
+ active = new Set();
27
+ pending = [];
28
+ acquire(spec) {
29
+ const request = normalize(spec);
30
+ if (request.shared.size === 0 && request.exclusive.size === 0) {
31
+ return Promise.resolve(() => { });
32
+ }
33
+ return new Promise((resolve) => {
34
+ this.pending.push({ ...request, resolve });
35
+ this.pump();
36
+ });
37
+ }
38
+ /** Acquire only when the scheduler is completely idle; otherwise skip. */
39
+ tryAcquire(spec) {
40
+ if (this.active.size > 0 || this.pending.length > 0)
41
+ return null;
42
+ const request = normalize(spec);
43
+ if (request.shared.size === 0 && request.exclusive.size === 0)
44
+ return () => { };
45
+ this.active.add(request);
46
+ return this.releaseFor(request);
47
+ }
48
+ pump() {
49
+ for (let i = 0; i < this.pending.length;) {
50
+ const candidate = this.pending[i];
51
+ const blockedByActive = [...this.active].some((active) => conflicts(candidate, active));
52
+ const blockedByEarlier = this.pending
53
+ .slice(0, i)
54
+ .some((earlier) => conflicts(candidate, earlier));
55
+ if (blockedByActive || blockedByEarlier) {
56
+ i++;
57
+ continue;
58
+ }
59
+ this.pending.splice(i, 1);
60
+ this.active.add(candidate);
61
+ candidate.resolve(this.releaseFor(candidate));
62
+ }
63
+ }
64
+ releaseFor(request) {
65
+ let released = false;
66
+ return () => {
67
+ if (released)
68
+ return;
69
+ released = true;
70
+ this.active.delete(request);
71
+ this.pump();
72
+ };
73
+ }
74
+ }
@@ -1,16 +1,16 @@
1
1
  import { mkdirSync, realpathSync } from "node:fs";
2
2
  import { join, sep } from "node:path";
3
3
  /**
4
- * Resolved absolute path to `<dataDir>/sessions/`. Pinned at server boot so
4
+ * Resolved absolute path to `<dataDir>/tasks/`. Pinned at server boot so
5
5
  * later `file://` URI construction and startsWith assertions all compare
6
- * against the same realpath (defends against macOS `/var /private/var`
6
+ * against the same realpath (defends against macOS `/var to /private/var`
7
7
  * symlink + any future symlink swaps under `data_dir`).
8
8
  *
9
9
  * Throws if the directory cannot be created or resolved — fail fast at boot
10
10
  * rather than later when an attachment dispatch tries to use it.
11
11
  */
12
- export function resolveSessionsAnchor(dataDir) {
13
- const dir = join(dataDir, "sessions");
12
+ export function resolveTasksAnchor(dataDir) {
13
+ const dir = join(dataDir, "tasks");
14
14
  mkdirSync(dir, { recursive: true });
15
15
  const real = realpathSync(dir);
16
16
  // Normalize trailing separator so `startsWith(anchor + sep)` is the
@@ -19,10 +19,11 @@ export function resolveSessionsAnchor(dataDir) {
19
19
  }
20
20
  /**
21
21
  * Returns true iff `realpath` is a strict descendant of
22
- * `<sessionsAnchor>/<sessionId>/attachments/`. Both args must already be
22
+ * `<tasksAnchor>/<taskId>/attachments/` (on-disk directory is still
23
+ * `sessions`). Both args must already be
23
24
  * realpath-resolved (no `..`, no symlinks left).
24
25
  */
25
- export function isInsideSessionAttachments(sessionsAnchor, sessionId, realpath) {
26
- const expected = sessionsAnchor + sep + sessionId + sep + "attachments" + sep;
26
+ export function isInsideTaskAttachments(tasksAnchor, taskId, realpath) {
27
+ const expected = tasksAnchor + sep + taskId + sep + "attachments" + sep;
27
28
  return realpath.startsWith(expected);
28
29
  }
package/lib/tokens.js CHANGED
@@ -11,7 +11,7 @@ import { randomBytes } from "node:crypto";
11
11
  * - Internal IDs that aren't auth-bearing (client IDs, request IDs,
12
12
  * error correlation IDs) — see sse-manager.ts, routes.ts, etc.
13
13
  * - HMAC signing keys (image URL secret) — see server.ts.
14
- * - UUIDs used as opaque labels (session IDs, msg IDs) — these
14
+ * - UUIDs used as opaque labels (task IDs, msg IDs) — these
15
15
  * are protocol identifiers, not credentials.
16
16
  *
17
17
  * Repo convention: `node:crypto` only. No `nanoid` / `uuid` deps.
package/lib/types.js CHANGED
@@ -3,10 +3,10 @@ import { z } from "zod";
3
3
  const FROM_REF_ALLOWED = /^(cron|external):[A-Za-z0-9._\-+/]{1,120}$/;
4
4
  export const MessageIngressSchema = z.object({
5
5
  from_ref: z.string().min(1).max(128).regex(FROM_REF_ALLOWED, {
6
- message: "from_ref must start with 'cron:' or 'external:' (reserved values and 'session:<id>' rejected in MVP)",
6
+ message: "from_ref must start with 'cron:' or 'external:' (reserved values and 'task:<id>' rejected in MVP)",
7
7
  }),
8
8
  from_label: z.string().max(64).optional(),
9
- to: z.union([z.literal("user"), z.string().regex(/^session:[^\s]+$/)]),
9
+ to: z.union([z.literal("user"), z.string().regex(/^task:[^\s]+$/)]),
10
10
  deliver: z.enum(["silent", "inapp", "push"]).default("push"),
11
11
  dedup_key: z.string().max(128).optional(),
12
12
  title: z.string().min(1).max(256),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lelouchhe/webagent",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "A terminal-style web UI for ACP-compatible agents",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -60,10 +60,12 @@
60
60
  },
61
61
  "dependencies": {
62
62
  "@agentclientprotocol/sdk": "^0.25.0",
63
+ "@modelcontextprotocol/sdk": "^1.30.0",
63
64
  "@types/proper-lockfile": "^4.1.4",
64
65
  "@types/web-push": "^3.6.4",
65
66
  "better-sqlite3": "^12.6.2",
66
67
  "busboy": "^1.6.0",
68
+ "diff": "^9.0.0",
67
69
  "dompurify": "^3.4.1",
68
70
  "file-type": "^22.0.1",
69
71
  "highlight.js": "^11.11.1",
@@ -91,5 +93,10 @@
91
93
  "prettier": "^3.8.3",
92
94
  "typescript": "^5.9.3",
93
95
  "typescript-eslint": "^8.59.0"
96
+ },
97
+ "allowScripts": {
98
+ "better-sqlite3@12.6.2": true,
99
+ "esbuild@0.27.3": true,
100
+ "fsevents@2.3.2": true
94
101
  }
95
102
  }