@beeos-ai/device-agent 0.5.1 → 0.7.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 (51) hide show
  1. package/.pi/extensions/android-tools.ts +24 -3
  2. package/dist/adapters/agent-gateway-client.d.ts +25 -0
  3. package/dist/adapters/agent-gateway-client.js +49 -0
  4. package/dist/adapters/agent-gateway-client.js.map +1 -1
  5. package/dist/adapters/user-file-uploader.d.ts +74 -0
  6. package/dist/adapters/user-file-uploader.js +78 -0
  7. package/dist/adapters/user-file-uploader.js.map +1 -0
  8. package/dist/app/in-flight-turn.d.ts +120 -0
  9. package/dist/app/in-flight-turn.js +158 -0
  10. package/dist/app/in-flight-turn.js.map +1 -0
  11. package/dist/app/jsonl-store.d.ts +109 -0
  12. package/dist/app/jsonl-store.js +402 -0
  13. package/dist/app/jsonl-store.js.map +1 -0
  14. package/dist/app/message-store.d.ts +82 -31
  15. package/dist/app/message-store.js +109 -48
  16. package/dist/app/message-store.js.map +1 -1
  17. package/dist/app/plugin-commands/registry.d.ts +41 -0
  18. package/dist/app/plugin-commands/registry.js +62 -0
  19. package/dist/app/plugin-commands/registry.js.map +1 -0
  20. package/dist/app/plugin-commands/screenshot-command.d.ts +32 -0
  21. package/dist/app/plugin-commands/screenshot-command.js +44 -0
  22. package/dist/app/plugin-commands/screenshot-command.js.map +1 -0
  23. package/dist/app/plugin-commands/types.d.ts +63 -0
  24. package/dist/app/plugin-commands/types.js +26 -0
  25. package/dist/app/plugin-commands/types.js.map +1 -0
  26. package/dist/app/screenshot-service.d.ts +68 -0
  27. package/dist/app/screenshot-service.js +64 -0
  28. package/dist/app/screenshot-service.js.map +1 -0
  29. package/dist/app/session-manager.d.ts +150 -55
  30. package/dist/app/session-manager.js +385 -122
  31. package/dist/app/session-manager.js.map +1 -1
  32. package/dist/bootstrap.js +5 -1
  33. package/dist/bootstrap.js.map +1 -1
  34. package/dist/config/paths.d.ts +48 -0
  35. package/dist/config/paths.js +59 -0
  36. package/dist/config/paths.js.map +1 -0
  37. package/dist/inbound/acp/handlers/device.d.ts +9 -1
  38. package/dist/inbound/acp/handlers/device.js +9 -1
  39. package/dist/inbound/acp/handlers/device.js.map +1 -1
  40. package/dist/inbound/acp/handlers/initialize.d.ts +17 -5
  41. package/dist/inbound/acp/handlers/initialize.js +19 -7
  42. package/dist/inbound/acp/handlers/initialize.js.map +1 -1
  43. package/dist/inbound/acp/handlers/session.d.ts +0 -1
  44. package/dist/inbound/acp/handlers/skills.d.ts +24 -2
  45. package/dist/inbound/acp/handlers/skills.js +31 -2
  46. package/dist/inbound/acp/handlers/skills.js.map +1 -1
  47. package/dist/orchestrator-system-prompt.js +2 -2
  48. package/dist/plugins/acp/index.d.ts +26 -1
  49. package/dist/plugins/acp/index.js +86 -3
  50. package/dist/plugins/acp/index.js.map +1 -1
  51. package/package.json +1 -1
@@ -1,21 +1,38 @@
1
1
  /**
2
- * In-memory MessageStore.
2
+ * In-memory MessageStore — typed turn-grained transcript log.
3
3
  *
4
- * Mirrors the surface of the Python `MessageStore` (`add_message`,
5
- * `get_messages_as_dicts`, `list_sessions`, etc.) so the ACP handlers stay
6
- * byte-for-byte compatible. Persistence is left out of the MVP; `~/.beeos`
7
- * SQLite can be re-added later behind the same interface (pluggable adapter).
4
+ * Each session holds an ordered list of `PersistedRecord`s, one per
5
+ * logical turn:
6
+ *
7
+ * - `user_message` — emitted on `session/prompt` entry.
8
+ * - `assistant_turn` — emitted exactly once per `task:end` (replaces
9
+ * the legacy "one row per chunk" model that produced fragmented
10
+ * transcripts on reload). Carries the full `text`, every tool call
11
+ * in invocation order, the stop reason, and an optional terminal
12
+ * error string.
13
+ *
14
+ * This base class is pure in-memory; persistence is added by
15
+ * `JsonlMessageStore` which extends this and forwards every mutation
16
+ * to disk before returning.
17
+ *
18
+ * The shape is deliberately aligned with OpenClaw's per-agent JSONL
19
+ * (`~/.openclaw/agents/<id>/sessions/<sid>.jsonl`) so operational
20
+ * tooling that already knows that layout works here too.
8
21
  */
9
22
  import { randomUUID } from "node:crypto";
23
+ /** Schema version. Bumped only on incompatible disk format changes. */
24
+ export const RECORD_SCHEMA_VERSION = 1;
25
+ /** Mutable in-memory message store. */
10
26
  export class MessageStore {
11
- maxPerSession;
27
+ maxRecordsPerSession;
12
28
  maxSessions;
13
29
  sessions = new Map();
14
- messages = new Map();
30
+ records = new Map();
15
31
  constructor(opts = {}) {
16
- this.maxPerSession = opts.maxMessagesPerSession ?? 100;
32
+ this.maxRecordsPerSession = opts.maxRecordsPerSession ?? 200;
17
33
  this.maxSessions = opts.maxSessions ?? 200;
18
34
  }
35
+ // ── session metadata ─────────────────────────────────────────────────────
19
36
  upsertSession(sessionId, title = "New chat") {
20
37
  const now = Date.now();
21
38
  const cur = this.sessions.get(sessionId);
@@ -31,14 +48,21 @@ export class MessageStore {
31
48
  }
32
49
  renameSession(sessionId, title) {
33
50
  const s = this.sessions.get(sessionId);
34
- if (s) {
35
- s.title = title;
36
- s.updatedAt = Date.now();
37
- }
51
+ if (!s)
52
+ return;
53
+ s.title = title;
54
+ s.updatedAt = Date.now();
38
55
  }
39
56
  deleteSession(sessionId) {
40
57
  this.sessions.delete(sessionId);
41
- this.messages.delete(sessionId);
58
+ this.records.delete(sessionId);
59
+ }
60
+ /** Drop every record in a session but keep the session record itself. */
61
+ clearSession(sessionId) {
62
+ this.records.delete(sessionId);
63
+ const s = this.sessions.get(sessionId);
64
+ if (s)
65
+ s.updatedAt = Date.now();
42
66
  }
43
67
  listSessions(limit = 50) {
44
68
  const all = Array.from(this.sessions.values())
@@ -46,44 +70,81 @@ export class MessageStore {
46
70
  .slice(0, limit);
47
71
  return all.map((s) => ({ sessionId: s.sessionId, title: s.title, updatedAt: s.updatedAt }));
48
72
  }
49
- clearSession(sessionId) {
50
- this.messages.delete(sessionId);
73
+ // ── record append ────────────────────────────────────────────────────────
74
+ /** Append a user prompt row. Returns the persisted record. */
75
+ appendUserMessage(sessionId, content, opts = {}) {
76
+ const ts = opts.now ?? Date.now();
77
+ const record = {
78
+ v: RECORD_SCHEMA_VERSION,
79
+ type: "user_message",
80
+ id: randomUUID(),
81
+ ts,
82
+ content,
83
+ ...(opts.turnId ? { turnId: opts.turnId } : {}),
84
+ ...(opts.messageId ? { messageId: opts.messageId } : {}),
85
+ };
86
+ this.persistRecord(sessionId, record, ts);
87
+ return record;
51
88
  }
52
- addMessage(sessionId, role, content, messageType = "text", metadata = {}) {
53
- const id = randomUUID();
54
- const now = Date.now();
55
- const entry = { id, sessionId, role, content, messageType, metadata, createdAt: now };
56
- const arr = this.messages.get(sessionId) ?? [];
57
- arr.push(entry);
58
- if (arr.length > this.maxPerSession)
59
- arr.splice(0, arr.length - this.maxPerSession);
60
- this.messages.set(sessionId, arr);
61
- const s = this.sessions.get(sessionId);
62
- if (s)
63
- s.updatedAt = now;
64
- else
65
- this.upsertSession(sessionId);
66
- return id;
89
+ /** Append a finalized assistant turn. Returns the persisted record. */
90
+ appendAssistantTurn(sessionId, turn, opts = {}) {
91
+ const ts = opts.now ?? turn.completedAt ?? Date.now();
92
+ const record = {
93
+ ...turn,
94
+ v: RECORD_SCHEMA_VERSION,
95
+ type: "assistant_turn",
96
+ id: randomUUID(),
97
+ ts,
98
+ };
99
+ this.persistRecord(sessionId, record, ts);
100
+ return record;
101
+ }
102
+ /** Pre-loaded record (used by `JsonlMessageStore.open` to seed memory). */
103
+ loadRecord(sessionId, record) {
104
+ this.persistRecord(sessionId, record, record.ts, /*touch*/ false);
105
+ }
106
+ /** All records for a session in chronological insertion order. */
107
+ getTurns(sessionId) {
108
+ return [...(this.records.get(sessionId) ?? [])];
67
109
  }
68
- getMessages(sessionId, limit) {
69
- const arr = this.messages.get(sessionId) ?? [];
70
- const n = Math.min(limit ?? this.maxPerSession, arr.length);
71
- return arr.slice(arr.length - n);
110
+ /** True iff at least one record exists for this session. */
111
+ hasRecords(sessionId) {
112
+ const arr = this.records.get(sessionId);
113
+ return !!arr && arr.length > 0;
72
114
  }
73
- getMessagesAsDicts(sessionId, limit) {
74
- return this.getMessages(sessionId, limit).map((m) => {
75
- const out = {
76
- role: m.role,
77
- content: m.content,
78
- type: m.messageType,
79
- createdAt: m.createdAt,
80
- };
81
- if (Object.keys(m.metadata).length > 0)
82
- out.metadata = m.metadata;
83
- return out;
84
- });
115
+ // ── internals ────────────────────────────────────────────────────────────
116
+ /**
117
+ * Append a record to memory and bump session.updatedAt.
118
+ * Subclasses (JsonlMessageStore) override to additionally persist.
119
+ *
120
+ * `touch=false` is used by `loadRecord` so a cold-start replay of a
121
+ * year-old transcript doesn't artificially bump updatedAt to "now".
122
+ */
123
+ persistRecord(sessionId, record, ts, touch = true) {
124
+ const arr = this.records.get(sessionId) ?? [];
125
+ arr.push(record);
126
+ if (arr.length > this.maxRecordsPerSession) {
127
+ arr.splice(0, arr.length - this.maxRecordsPerSession);
128
+ }
129
+ this.records.set(sessionId, arr);
130
+ const s = this.sessions.get(sessionId);
131
+ if (s) {
132
+ if (touch)
133
+ s.updatedAt = ts;
134
+ else if (ts > s.updatedAt)
135
+ s.updatedAt = ts;
136
+ }
137
+ else {
138
+ this.sessions.set(sessionId, {
139
+ sessionId,
140
+ title: "New chat",
141
+ createdAt: ts,
142
+ updatedAt: ts,
143
+ });
144
+ this.evictSessionLru();
145
+ }
85
146
  }
86
- /** Drop the least-recently-updated sessions when we exceed maxSessions. */
147
+ /** Drop the least-recently-updated sessions when exceeding maxSessions. */
87
148
  evictSessionLru() {
88
149
  if (this.sessions.size <= this.maxSessions)
89
150
  return;
@@ -92,7 +153,7 @@ export class MessageStore {
92
153
  for (let i = 0; i < overflow; i++) {
93
154
  const id = sorted[i].sessionId;
94
155
  this.sessions.delete(id);
95
- this.messages.delete(id);
156
+ this.records.delete(id);
96
157
  }
97
158
  }
98
159
  }
@@ -1 +1 @@
1
- {"version":3,"file":"message-store.js","sourceRoot":"","sources":["../../src/app/message-store.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"message-store.js","sourceRoot":"","sources":["../../src/app/message-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * PluginCommandRegistry — name → handler lookup with prefix matching.
3
+ *
4
+ * Simplified port of OpenClaw's `matchPluginCommand`
5
+ * (`openclaw/src/plugins/commands.ts:62-100`). Drops some of the
6
+ * upstream high-end features (hyphen/underscore aliases, alternate
7
+ * invocation keys) — the device-agent only needs `/screenshot` today
8
+ * and we'll port more as needed.
9
+ *
10
+ * Match contract:
11
+ * - Input MUST start with `/`. Whitespace-only / non-slash strings
12
+ * short-circuit to `null` so natural-language prompts ("截屏发给
13
+ * 我") fall through unchanged to the AgentRunner.
14
+ * - Command name extraction: `/<name>` or `/<name> <args...>`.
15
+ * Names match case-insensitively (typing `/Screenshot` still
16
+ * hits the registered handler).
17
+ * - Argument gating: when the matched command has `acceptsArgs:
18
+ * false` (default), passing arguments DROPS the match — the line
19
+ * falls through to the AgentRunner so a misspelled builtin
20
+ * doesn't accidentally swallow real user intent.
21
+ */
22
+ import type { PluginCommand } from "./types.js";
23
+ export interface PluginCommandMatch {
24
+ command: PluginCommand;
25
+ args?: string;
26
+ }
27
+ export declare class PluginCommandRegistry {
28
+ private readonly commands;
29
+ /** Register (or replace) a command. Names are case-insensitive. */
30
+ register(cmd: PluginCommand): void;
31
+ /**
32
+ * Match a free-form prompt string against the registry. Returns the
33
+ * matched command + parsed args, or `null` when the prompt is not
34
+ * a slash command or doesn't match any registered name.
35
+ */
36
+ match(text: string): PluginCommandMatch | null;
37
+ /** Snapshot of registered commands, in insertion order. */
38
+ list(): PluginCommand[];
39
+ /** Number of registered commands — convenience for tests. */
40
+ size(): number;
41
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * PluginCommandRegistry — name → handler lookup with prefix matching.
3
+ *
4
+ * Simplified port of OpenClaw's `matchPluginCommand`
5
+ * (`openclaw/src/plugins/commands.ts:62-100`). Drops some of the
6
+ * upstream high-end features (hyphen/underscore aliases, alternate
7
+ * invocation keys) — the device-agent only needs `/screenshot` today
8
+ * and we'll port more as needed.
9
+ *
10
+ * Match contract:
11
+ * - Input MUST start with `/`. Whitespace-only / non-slash strings
12
+ * short-circuit to `null` so natural-language prompts ("截屏发给
13
+ * 我") fall through unchanged to the AgentRunner.
14
+ * - Command name extraction: `/<name>` or `/<name> <args...>`.
15
+ * Names match case-insensitively (typing `/Screenshot` still
16
+ * hits the registered handler).
17
+ * - Argument gating: when the matched command has `acceptsArgs:
18
+ * false` (default), passing arguments DROPS the match — the line
19
+ * falls through to the AgentRunner so a misspelled builtin
20
+ * doesn't accidentally swallow real user intent.
21
+ */
22
+ export class PluginCommandRegistry {
23
+ commands = new Map();
24
+ /** Register (or replace) a command. Names are case-insensitive. */
25
+ register(cmd) {
26
+ this.commands.set(cmd.name.toLowerCase(), cmd);
27
+ }
28
+ /**
29
+ * Match a free-form prompt string against the registry. Returns the
30
+ * matched command + parsed args, or `null` when the prompt is not
31
+ * a slash command or doesn't match any registered name.
32
+ */
33
+ match(text) {
34
+ const trimmed = text.trim();
35
+ if (!trimmed.startsWith("/"))
36
+ return null;
37
+ const spaceIdx = trimmed.indexOf(" ");
38
+ const rawName = spaceIdx === -1 ? trimmed.slice(1) : trimmed.slice(1, spaceIdx);
39
+ const name = rawName.toLowerCase();
40
+ if (!name)
41
+ return null;
42
+ const args = spaceIdx === -1 ? undefined : trimmed.slice(spaceIdx + 1).trim();
43
+ const command = this.commands.get(name);
44
+ if (!command)
45
+ return null;
46
+ // Reject extra args for commands that don't accept them — the line
47
+ // falls through to the AgentRunner so e.g. `/screenshot please`
48
+ // doesn't get swallowed silently.
49
+ if (args && !command.acceptsArgs)
50
+ return null;
51
+ return { command, args: args || undefined };
52
+ }
53
+ /** Snapshot of registered commands, in insertion order. */
54
+ list() {
55
+ return Array.from(this.commands.values());
56
+ }
57
+ /** Number of registered commands — convenience for tests. */
58
+ size() {
59
+ return this.commands.size;
60
+ }
61
+ }
62
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../../src/app/plugin-commands/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AASH,MAAM,OAAO,qBAAqB;IACf,QAAQ,GAAG,IAAI,GAAG,EAAyB,CAAC;IAE7D,mEAAmE;IACnE,QAAQ,CAAC,GAAkB;QACzB,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,GAAG,CAAC,CAAC;IACjD,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,IAAY;QAChB,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,OAAO,IAAI,CAAC;QAE1C,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACtC,MAAM,OAAO,GACX,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;QAClE,MAAM,IAAI,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;QACnC,IAAI,CAAC,IAAI;YAAE,OAAO,IAAI,CAAC;QAEvB,MAAM,IAAI,GACR,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAEnE,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACxC,IAAI,CAAC,OAAO;YAAE,OAAO,IAAI,CAAC;QAE1B,mEAAmE;QACnE,gEAAgE;QAChE,kCAAkC;QAClC,IAAI,IAAI,IAAI,CAAC,OAAO,CAAC,WAAW;YAAE,OAAO,IAAI,CAAC;QAE9C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,IAAI,SAAS,EAAE,CAAC;IAC9C,CAAC;IAED,2DAA2D;IAC3D,IAAI;QACF,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;IAC5C,CAAC;IAED,6DAA6D;IAC7D,IAAI;QACF,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;IAC5B,CAAC;CACF"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * `/screenshot` plugin command handler.
3
+ *
4
+ * Adapter layer between the ACP-unaware `ScreenshotService` and the
5
+ * session/notifier surface. Responsibilities are intentionally narrow:
6
+ *
7
+ * 1. Drive `ScreenshotService.captureAndUpload()` to stage the
8
+ * current device frame as a `beeos-file://${fileId}` attachment.
9
+ * 2. Translate the resulting `ChatAttachment` into an ACP
10
+ * `agent_message_chunk` carrying a `resource_link` block.
11
+ * 3. Push that update onto the caller's session via the injected
12
+ * `SessionUpdateNotifier`.
13
+ *
14
+ * Strict separation-of-concerns boundary:
15
+ * - DOES NOT touch MCP / `Buffer` / upload internals — those live
16
+ * entirely inside `ScreenshotService` + `UserFileUploader`.
17
+ * - Errors propagate up to `SessionManager` which renders them as a
18
+ * `system_message_chunk` notice (no toast logic here).
19
+ */
20
+ import type { ScreenshotService } from "../screenshot-service.js";
21
+ import type { SessionUpdateNotifier } from "../session-manager.js";
22
+ import type { PluginCommand, PluginCommandContext } from "./types.js";
23
+ export declare class ScreenshotCommandHandler implements PluginCommand {
24
+ private readonly service;
25
+ private readonly notifier;
26
+ readonly name = "screenshot";
27
+ readonly description = "Take a device screenshot and send it to the chat";
28
+ readonly emoji = "\uD83D\uDCF8";
29
+ readonly acceptsArgs = false;
30
+ constructor(service: ScreenshotService, notifier: SessionUpdateNotifier);
31
+ handle(ctx: PluginCommandContext): Promise<void>;
32
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * `/screenshot` plugin command handler.
3
+ *
4
+ * Adapter layer between the ACP-unaware `ScreenshotService` and the
5
+ * session/notifier surface. Responsibilities are intentionally narrow:
6
+ *
7
+ * 1. Drive `ScreenshotService.captureAndUpload()` to stage the
8
+ * current device frame as a `beeos-file://${fileId}` attachment.
9
+ * 2. Translate the resulting `ChatAttachment` into an ACP
10
+ * `agent_message_chunk` carrying a `resource_link` block.
11
+ * 3. Push that update onto the caller's session via the injected
12
+ * `SessionUpdateNotifier`.
13
+ *
14
+ * Strict separation-of-concerns boundary:
15
+ * - DOES NOT touch MCP / `Buffer` / upload internals — those live
16
+ * entirely inside `ScreenshotService` + `UserFileUploader`.
17
+ * - Errors propagate up to `SessionManager` which renders them as a
18
+ * `system_message_chunk` notice (no toast logic here).
19
+ */
20
+ export class ScreenshotCommandHandler {
21
+ service;
22
+ notifier;
23
+ name = "screenshot";
24
+ description = "Take a device screenshot and send it to the chat";
25
+ emoji = "📸";
26
+ acceptsArgs = false;
27
+ constructor(service, notifier) {
28
+ this.service = service;
29
+ this.notifier = notifier;
30
+ }
31
+ async handle(ctx) {
32
+ const att = await this.service.captureAndUpload();
33
+ this.notifier.send(ctx.sessionId, {
34
+ sessionUpdate: "agent_message_chunk",
35
+ content: {
36
+ type: "resource_link",
37
+ uri: att.uri,
38
+ name: att.fileName,
39
+ mimeType: att.mimeType,
40
+ },
41
+ });
42
+ }
43
+ }
44
+ //# sourceMappingURL=screenshot-command.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"screenshot-command.js","sourceRoot":"","sources":["../../../src/app/plugin-commands/screenshot-command.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AASH,MAAM,OAAO,wBAAwB;IAOhB;IACA;IAPV,IAAI,GAAG,YAAY,CAAC;IACpB,WAAW,GAAG,kDAAkD,CAAC;IACjE,KAAK,GAAG,IAAI,CAAC;IACb,WAAW,GAAG,KAAK,CAAC;IAE7B,YACmB,OAA0B,EAC1B,QAA+B;QAD/B,YAAO,GAAP,OAAO,CAAmB;QAC1B,aAAQ,GAAR,QAAQ,CAAuB;IAC/C,CAAC;IAEJ,KAAK,CAAC,MAAM,CAAC,GAAyB;QACpC,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,gBAAgB,EAAE,CAAC;QAClD,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE;YAChC,aAAa,EAAE,qBAAqB;YACpC,OAAO,EAAE;gBACP,IAAI,EAAE,eAAe;gBACrB,GAAG,EAAE,GAAG,CAAC,GAAG;gBACZ,IAAI,EAAE,GAAG,CAAC,QAAQ;gBAClB,QAAQ,EAAE,GAAG,CAAC,QAAQ;aACvB;SACF,CAAC,CAAC;IACL,CAAC;CACF"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Plugin Command abstraction — server-side, channel-agnostic slash
3
+ * commands that bypass the LLM / AgentRunner.
4
+ *
5
+ * Modelled directly after OpenClaw's `OpenClawPluginCommandDefinition`
6
+ * (see `openclaw/src/plugins/types.ts:1946-1980`):
7
+ *
8
+ * - Each command bundles its own `handle()` so the SessionManager
9
+ * only has to dispatch by name.
10
+ * - All channels (ACP web chat, future Telegram / Discord / CLI)
11
+ * deliver these as plain `/name [args]` text in `session/prompt`,
12
+ * pre-LLM. The registry is responsible for prefix matching.
13
+ * - Surface metadata (`name`, `description`, `emoji`) lives on the
14
+ * command itself so `skills/list` can be generated from a single
15
+ * source of truth.
16
+ *
17
+ * Strict separation-of-concerns boundary:
18
+ * - This module imports NOTHING from `inbound/acp/*` — the registry
19
+ * / commands stay protocol-agnostic so a future HTTP or CLI surface
20
+ * can drive them too.
21
+ * - Concrete command implementations (`screenshot-command.ts`, etc.)
22
+ * are free to import session-side ports (notifier) — they're the
23
+ * adapter layer between business capabilities and the protocol.
24
+ */
25
+ export interface PluginCommandContext {
26
+ /**
27
+ * The session to deliver the command's output to. SessionManager
28
+ * passes its own session id through; handlers use this when calling
29
+ * `notifier.notify(sessionId, …)` to push their result.
30
+ */
31
+ sessionId: string;
32
+ /**
33
+ * Trimmed argument string after the command name. Empty / undefined
34
+ * when the command was invoked bare (`/screenshot`). Only forwarded
35
+ * to commands that opt in via `acceptsArgs: true`.
36
+ */
37
+ args?: string;
38
+ }
39
+ export interface PluginCommand {
40
+ /** Command name without the leading slash (e.g. `"screenshot"`). */
41
+ readonly name: string;
42
+ /** Description shown in `skills/list` and slash-autocomplete UI. */
43
+ readonly description: string;
44
+ /** Optional emoji prefix for surface presentation (Web slash, /help). */
45
+ readonly emoji?: string;
46
+ /**
47
+ * Whether the command accepts arguments after its name. Commands
48
+ * with `acceptsArgs: false` only match bare `/name` invocations —
49
+ * `/name foo` falls through to the AgentRunner so a misspelled
50
+ * builtin doesn't accidentally swallow user intent.
51
+ */
52
+ readonly acceptsArgs?: boolean;
53
+ /**
54
+ * Execute the command. Bypasses LLM / AgentRunner — the handler is
55
+ * responsible for emitting any session-side notifications it wants
56
+ * the user to see (typically through the injected notifier).
57
+ *
58
+ * Throwing here surfaces as a `system_message_chunk` error notice
59
+ * via SessionManager's central error handler — handlers SHOULD let
60
+ * exceptions propagate rather than swallow them silently.
61
+ */
62
+ handle(ctx: PluginCommandContext): Promise<void>;
63
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Plugin Command abstraction — server-side, channel-agnostic slash
3
+ * commands that bypass the LLM / AgentRunner.
4
+ *
5
+ * Modelled directly after OpenClaw's `OpenClawPluginCommandDefinition`
6
+ * (see `openclaw/src/plugins/types.ts:1946-1980`):
7
+ *
8
+ * - Each command bundles its own `handle()` so the SessionManager
9
+ * only has to dispatch by name.
10
+ * - All channels (ACP web chat, future Telegram / Discord / CLI)
11
+ * deliver these as plain `/name [args]` text in `session/prompt`,
12
+ * pre-LLM. The registry is responsible for prefix matching.
13
+ * - Surface metadata (`name`, `description`, `emoji`) lives on the
14
+ * command itself so `skills/list` can be generated from a single
15
+ * source of truth.
16
+ *
17
+ * Strict separation-of-concerns boundary:
18
+ * - This module imports NOTHING from `inbound/acp/*` — the registry
19
+ * / commands stay protocol-agnostic so a future HTTP or CLI surface
20
+ * can drive them too.
21
+ * - Concrete command implementations (`screenshot-command.ts`, etc.)
22
+ * are free to import session-side ports (notifier) — they're the
23
+ * adapter layer between business capabilities and the protocol.
24
+ */
25
+ export {};
26
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/app/plugin-commands/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * ScreenshotService — business-capability layer for "take a device
3
+ * screenshot and stage it as a user-visible chat attachment".
4
+ *
5
+ * mcp.screenshot() ──▶ bytes ──▶ uploader.upload() ──▶ ChatAttachment
6
+ *
7
+ * Strict separation-of-concerns boundary:
8
+ *
9
+ * - Inputs / outputs are platform-neutral. No `AcpMessage`, no
10
+ * `sessionId`, no `notifier` ever crosses this module.
11
+ * - The returned `ChatAttachment` is the lingua franca passed up to
12
+ * whoever needs to render the result on a session: the ACP plugin
13
+ * command adapter (`ScreenshotCommandHandler`), a future HTTP
14
+ * handler, the AgentLoop, etc.
15
+ *
16
+ * This is the device-agent equivalent of beeos-claw's `uploadLocalFile`
17
+ * pipeline (`agents/beeos-claw/src/upload/upload-file.ts`) — same
18
+ * 3-step Agent Gateway flow (presign + PUT + confirm), different
19
+ * source (live device frame instead of a local file).
20
+ */
21
+ import type { IMcpRuntime } from "../mcp-client/runtime.js";
22
+ import type { UserFileUploader } from "../adapters/user-file-uploader.js";
23
+ export interface ChatAttachment {
24
+ /** Stable platform fileId returned by the Agent Gateway after confirm. */
25
+ fileId: string;
26
+ /** `beeos-file://${fileId}` — what goes into ACP `resource_link.uri`. */
27
+ uri: string;
28
+ mimeType: string;
29
+ fileName: string;
30
+ format: "png" | "jpeg";
31
+ /** Decoded pixel width (matches `b64` payload, may differ from device click frame). */
32
+ width?: number;
33
+ /** Decoded pixel height. */
34
+ height?: number;
35
+ }
36
+ export interface CaptureOpts {
37
+ /**
38
+ * Storage key prefix override. Defaults to `device-screenshots/` so
39
+ * S3 lifecycle rules can target screenshots independently of other
40
+ * user-attachment categories.
41
+ */
42
+ keyPrefix?: string;
43
+ }
44
+ /** Test seam — defaults to `() => new Date()`. */
45
+ type Clock = () => Date;
46
+ export interface ScreenshotServiceOptions {
47
+ /** Test seam. Defaults to `() => new Date()`. */
48
+ now?: Clock;
49
+ }
50
+ export declare class ScreenshotService {
51
+ private readonly mcp;
52
+ private readonly uploader;
53
+ private readonly now;
54
+ constructor(mcp: IMcpRuntime, uploader: UserFileUploader, opts?: ScreenshotServiceOptions);
55
+ /**
56
+ * Capture the current device screen and stage it as a user-visible
57
+ * chat attachment. Returns a `ChatAttachment` with a stable
58
+ * `beeos-file://` URI and metadata. ACP / session knowledge stays
59
+ * out of this method by design.
60
+ *
61
+ * Errors:
62
+ * - MCP runtime errors propagate from `mcp.screenshot()`.
63
+ * - Upload errors propagate from `uploader.upload()` (typically
64
+ * `AgentGatewayError` with the failing step's HTTP status).
65
+ */
66
+ captureAndUpload(opts?: CaptureOpts): Promise<ChatAttachment>;
67
+ }
68
+ export {};
@@ -0,0 +1,64 @@
1
+ /**
2
+ * ScreenshotService — business-capability layer for "take a device
3
+ * screenshot and stage it as a user-visible chat attachment".
4
+ *
5
+ * mcp.screenshot() ──▶ bytes ──▶ uploader.upload() ──▶ ChatAttachment
6
+ *
7
+ * Strict separation-of-concerns boundary:
8
+ *
9
+ * - Inputs / outputs are platform-neutral. No `AcpMessage`, no
10
+ * `sessionId`, no `notifier` ever crosses this module.
11
+ * - The returned `ChatAttachment` is the lingua franca passed up to
12
+ * whoever needs to render the result on a session: the ACP plugin
13
+ * command adapter (`ScreenshotCommandHandler`), a future HTTP
14
+ * handler, the AgentLoop, etc.
15
+ *
16
+ * This is the device-agent equivalent of beeos-claw's `uploadLocalFile`
17
+ * pipeline (`agents/beeos-claw/src/upload/upload-file.ts`) — same
18
+ * 3-step Agent Gateway flow (presign + PUT + confirm), different
19
+ * source (live device frame instead of a local file).
20
+ */
21
+ export class ScreenshotService {
22
+ mcp;
23
+ uploader;
24
+ now;
25
+ constructor(mcp, uploader, opts = {}) {
26
+ this.mcp = mcp;
27
+ this.uploader = uploader;
28
+ this.now = opts.now ?? (() => new Date());
29
+ }
30
+ /**
31
+ * Capture the current device screen and stage it as a user-visible
32
+ * chat attachment. Returns a `ChatAttachment` with a stable
33
+ * `beeos-file://` URI and metadata. ACP / session knowledge stays
34
+ * out of this method by design.
35
+ *
36
+ * Errors:
37
+ * - MCP runtime errors propagate from `mcp.screenshot()`.
38
+ * - Upload errors propagate from `uploader.upload()` (typically
39
+ * `AgentGatewayError` with the failing step's HTTP status).
40
+ */
41
+ async captureAndUpload(opts = {}) {
42
+ const ss = await this.mcp.screenshot();
43
+ const bytes = Buffer.from(ss.b64, "base64");
44
+ const mimeType = ss.format === "png" ? "image/png" : "image/jpeg";
45
+ const ts = this.now().toISOString().replace(/[:.]/g, "-");
46
+ const fileName = `screenshot-${ts}.${ss.format}`;
47
+ const uploaded = await this.uploader.upload({
48
+ bytes,
49
+ mimeType,
50
+ fileName,
51
+ keyPrefix: opts.keyPrefix ?? "device-screenshots/",
52
+ });
53
+ return {
54
+ fileId: uploaded.fileId,
55
+ uri: uploaded.uri,
56
+ mimeType: uploaded.mimeType,
57
+ fileName: uploaded.fileName,
58
+ format: ss.format,
59
+ width: ss.width,
60
+ height: ss.height,
61
+ };
62
+ }
63
+ }
64
+ //# sourceMappingURL=screenshot-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"screenshot-service.js","sourceRoot":"","sources":["../../src/app/screenshot-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAoCH,MAAM,OAAO,iBAAiB;IAIT;IACA;IAJF,GAAG,CAAQ;IAE5B,YACmB,GAAgB,EAChB,QAA0B,EAC3C,OAAiC,EAAE;QAFlB,QAAG,GAAH,GAAG,CAAa;QAChB,aAAQ,GAAR,QAAQ,CAAkB;QAG3C,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;IAC5C,CAAC;IAED;;;;;;;;;;OAUG;IACH,KAAK,CAAC,gBAAgB,CAAC,OAAoB,EAAE;QAC3C,MAAM,EAAE,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,CAAC;QAEvC,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;QAC5C,MAAM,QAAQ,GACZ,EAAE,CAAC,MAAM,KAAK,KAAK,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,YAAY,CAAC;QACnD,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;QAC1D,MAAM,QAAQ,GAAG,cAAc,EAAE,IAAI,EAAE,CAAC,MAAM,EAAE,CAAC;QAEjD,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;YAC1C,KAAK;YACL,QAAQ;YACR,QAAQ;YACR,SAAS,EAAE,IAAI,CAAC,SAAS,IAAI,qBAAqB;SACnD,CAAC,CAAC;QAEH,OAAO;YACL,MAAM,EAAE,QAAQ,CAAC,MAAM;YACvB,GAAG,EAAE,QAAQ,CAAC,GAAG;YACjB,QAAQ,EAAE,QAAQ,CAAC,QAAQ;YAC3B,QAAQ,EAAE,QAAQ,CAAC,QAAQ;YAC3B,MAAM,EAAE,EAAE,CAAC,MAAM;YACjB,KAAK,EAAE,EAAE,CAAC,KAAK;YACf,MAAM,EAAE,EAAE,CAAC,MAAM;SAClB,CAAC;IACJ,CAAC;CACF"}