agent-sin 0.1.12 → 0.1.16

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 (97) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/README.md +2 -1
  3. package/builtin-skills/_shared/_todo_lib.py +290 -0
  4. package/builtin-skills/even-g2-setup/main.ts +896 -0
  5. package/builtin-skills/even-g2-setup/skill.yaml +133 -0
  6. package/builtin-skills/memo-delete/main.py +28 -107
  7. package/builtin-skills/memo-delete/skill.yaml +10 -21
  8. package/builtin-skills/memo-index/main.py +96 -64
  9. package/builtin-skills/memo-index/skill.yaml +4 -10
  10. package/builtin-skills/memo-list/main.py +126 -72
  11. package/builtin-skills/memo-list/skill.yaml +8 -14
  12. package/builtin-skills/memo-save/main.py +191 -25
  13. package/builtin-skills/memo-save/skill.yaml +29 -5
  14. package/builtin-skills/memo-search/main.py +38 -18
  15. package/builtin-skills/memo-vector-search/main.py +11 -6
  16. package/builtin-skills/nightly-topic-knowledge/_feedback_lib.py +391 -0
  17. package/builtin-skills/nightly-topic-knowledge/_topics_lib.py +415 -0
  18. package/builtin-skills/nightly-topic-knowledge/main.py +403 -0
  19. package/builtin-skills/nightly-topic-knowledge/skill.yaml +88 -0
  20. package/builtin-skills/schedule-add/main.py +26 -0
  21. package/builtin-skills/service-restart/main.ts +249 -0
  22. package/builtin-skills/service-restart/skill.yaml +49 -0
  23. package/builtin-skills/todo-add/main.py +3 -1
  24. package/builtin-skills/todo-delete/main.py +3 -1
  25. package/builtin-skills/todo-done/main.py +3 -1
  26. package/builtin-skills/todo-list/main.py +4 -1
  27. package/builtin-skills/todo-tick/main.py +3 -1
  28. package/builtin-skills/topic-knowledge-read/main.py +118 -0
  29. package/builtin-skills/topic-knowledge-read/skill.yaml +49 -0
  30. package/dist/builder/build-action-classifier.d.ts +18 -0
  31. package/dist/builder/build-action-classifier.js +82 -1
  32. package/dist/builder/build-flow.d.ts +33 -4
  33. package/dist/builder/build-flow.js +251 -89
  34. package/dist/builder/builder-session.d.ts +1 -1
  35. package/dist/builder/builder-session.js +112 -7
  36. package/dist/builder/conversation-router.d.ts +4 -2
  37. package/dist/builder/conversation-router.js +19 -2
  38. package/dist/cli/index.js +323 -20
  39. package/dist/core/ai-provider.d.ts +1 -0
  40. package/dist/core/ai-provider.js +8 -3
  41. package/dist/core/chat-engine.d.ts +9 -3
  42. package/dist/core/chat-engine.js +1263 -146
  43. package/dist/core/config.d.ts +4 -0
  44. package/dist/core/config.js +82 -0
  45. package/dist/core/daily-memory-promotion.d.ts +7 -0
  46. package/dist/core/daily-memory-promotion.js +596 -18
  47. package/dist/core/image-attachments.d.ts +31 -0
  48. package/dist/core/image-attachments.js +237 -0
  49. package/dist/core/logger.d.ts +2 -1
  50. package/dist/core/logger.js +77 -1
  51. package/dist/core/memo-migration.d.ts +3 -0
  52. package/dist/core/memo-migration.js +422 -0
  53. package/dist/core/native-modules.d.ts +24 -0
  54. package/dist/core/native-modules.js +99 -0
  55. package/dist/core/notifier.d.ts +8 -3
  56. package/dist/core/notifier.js +191 -17
  57. package/dist/core/obsidian-vault.d.ts +19 -0
  58. package/dist/core/obsidian-vault.js +477 -0
  59. package/dist/core/operating-model.d.ts +2 -0
  60. package/dist/core/operating-model.js +15 -0
  61. package/dist/core/output-writer.d.ts +3 -2
  62. package/dist/core/output-writer.js +108 -7
  63. package/dist/core/profile-memory.js +22 -1
  64. package/dist/core/runtime.d.ts +2 -0
  65. package/dist/core/runtime.js +9 -1
  66. package/dist/core/secrets.d.ts +4 -0
  67. package/dist/core/secrets.js +34 -0
  68. package/dist/core/skill-history.d.ts +44 -0
  69. package/dist/core/skill-history.js +329 -0
  70. package/dist/core/skill-registry.d.ts +5 -0
  71. package/dist/core/skill-registry.js +11 -0
  72. package/dist/discord/bot.d.ts +1 -0
  73. package/dist/discord/bot.js +181 -10
  74. package/dist/even-g2/gateway.d.ts +15 -0
  75. package/dist/even-g2/gateway.js +868 -0
  76. package/dist/runtimes/codex-app-server.d.ts +5 -1
  77. package/dist/runtimes/codex-app-server.js +147 -8
  78. package/dist/runtimes/python-runner.js +82 -0
  79. package/dist/runtimes/typescript-runner.js +13 -1
  80. package/dist/skills-sdk/types.d.ts +19 -4
  81. package/dist/telegram/bot.d.ts +1 -0
  82. package/dist/telegram/bot.js +115 -7
  83. package/package.json +3 -1
  84. package/templates/even-g2-agent/README.md +83 -0
  85. package/templates/even-g2-agent/app.json +20 -0
  86. package/templates/even-g2-agent/index.html +31 -0
  87. package/templates/even-g2-agent/package-lock.json +1836 -0
  88. package/templates/even-g2-agent/package.json +22 -0
  89. package/templates/even-g2-agent/scripts/qr-auto.mjs +182 -0
  90. package/templates/even-g2-agent/src/embedded-config.ts +4 -0
  91. package/templates/even-g2-agent/src/main.ts +539 -0
  92. package/templates/even-g2-agent/src/style.css +70 -0
  93. package/templates/even-g2-agent/tsconfig.json +11 -0
  94. package/templates/skill-python/main.py +20 -2
  95. package/templates/skill-python/skill.yaml +9 -0
  96. package/templates/skill-typescript/main.ts +40 -5
  97. package/templates/skill-typescript/skill.yaml +9 -0
@@ -0,0 +1,539 @@
1
+ import {
2
+ LAUNCH_SOURCE_GLASSES_MENU,
3
+ OsEventTypeList,
4
+ StartUpPageCreateResult,
5
+ waitForEvenAppBridge,
6
+ type EvenAppBridge,
7
+ type EvenHubEvent,
8
+ } from "@evenrealities/even_hub_sdk";
9
+ import { EMBEDDED_SERVER, EMBEDDED_TOKEN } from "./embedded-config";
10
+ import "./style.css";
11
+
12
+ const STORAGE_SERVER = "agent_sin_g2_server";
13
+ const STORAGE_TOKEN = "agent_sin_g2_token";
14
+ const IDLE_ACTIONS = ["Talk"] as const;
15
+ const RECORDING_ACTIONS = ["Send", "Cancel"] as const;
16
+ const THINKING_ACTIONS = ["Cancel"] as const;
17
+
18
+ type ActionName = "Talk" | "Send" | "Cancel";
19
+
20
+ function currentMenu(): readonly ActionName[] {
21
+ if (recording) return RECORDING_ACTIONS as readonly ActionName[];
22
+ if (thinking) return THINKING_ACTIONS as readonly ActionName[];
23
+ return IDLE_ACTIONS as readonly ActionName[];
24
+ }
25
+
26
+ const statusEl = document.querySelector<HTMLParagraphElement>("#status")!;
27
+ const logEl = document.querySelector<HTMLPreElement>("#log")!;
28
+ const settingsForm = document.querySelector<HTMLFormElement>("#settings")!;
29
+ const textForm = document.querySelector<HTMLFormElement>("#textForm")!;
30
+ const serverInput = document.querySelector<HTMLInputElement>("#serverUrl")!;
31
+ const tokenInput = document.querySelector<HTMLInputElement>("#token")!;
32
+ const textInput = document.querySelector<HTMLInputElement>("#textInput")!;
33
+
34
+ let bridge: EvenAppBridge | null = null;
35
+ let ws: WebSocket | null = null;
36
+ let recording = false;
37
+ let thinking = false;
38
+ let ignoreNextReply = false;
39
+ let audioChunks: Uint8Array[] = [];
40
+ let glassesReady = false;
41
+ let selectedActionIndex = 0;
42
+ let glassesStatusText = "Agent ready";
43
+ let pendingTranscript: string | null = null;
44
+
45
+ void boot();
46
+
47
+ async function boot(): Promise<void> {
48
+ const urlConfig = applyUrlConfig();
49
+ serverInput.value = localStorage.getItem(STORAGE_SERVER) || "";
50
+ tokenInput.value = localStorage.getItem(STORAGE_TOKEN) || "";
51
+ settingsForm.addEventListener("submit", (event) => {
52
+ event.preventDefault();
53
+ void saveSettings();
54
+ });
55
+ textForm.addEventListener("submit", (event) => {
56
+ event.preventDefault();
57
+ const text = textInput.value.trim();
58
+ if (!text) return;
59
+ textInput.value = "";
60
+ const sent = sendJson({ type: "message", text, input_mode: "text" });
61
+ appendLog(`G2: ${text}`);
62
+ if (sent) {
63
+ thinking = true;
64
+ void renderGlasses("Thinking...");
65
+ }
66
+ });
67
+
68
+ connect();
69
+ bridge = await waitForBridge(5000);
70
+ if (!bridge) {
71
+ setStatus(serverInput.value.trim() ? "Phone mode ready" : "Set server URL");
72
+ return;
73
+ }
74
+ if (urlConfig.server) await writeSetting(STORAGE_SERVER, urlConfig.server);
75
+ if (urlConfig.token) await writeSetting(STORAGE_TOKEN, urlConfig.token);
76
+ serverInput.value = await readSetting(STORAGE_SERVER);
77
+ tokenInput.value = await readSetting(STORAGE_TOKEN);
78
+ bridge.onLaunchSource((source) => {
79
+ if (source === LAUNCH_SOURCE_GLASSES_MENU) {
80
+ void startGlassesMode();
81
+ } else {
82
+ setStatus("Set server URL, then launch from glasses.");
83
+ }
84
+ });
85
+ bridge.onEvenHubEvent((event) => {
86
+ void handleEvenHubEvent(event);
87
+ });
88
+ connect();
89
+
90
+ // Fallback: the launch-source event may already have fired before our listener
91
+ // is registered when the webview is cold-started from the glasses menu. If the
92
+ // user has a server URL set, just bring up the glasses UI proactively.
93
+ const initialServer = await readSetting(STORAGE_SERVER);
94
+ if (initialServer && !glassesReady) {
95
+ void startGlassesMode();
96
+ }
97
+ }
98
+
99
+ function applyUrlConfig(): { server: string; token: string } {
100
+ const params = new URLSearchParams(window.location.search);
101
+ const hash = window.location.hash.startsWith("#")
102
+ ? new URLSearchParams(window.location.hash.slice(1))
103
+ : new URLSearchParams();
104
+ const server = (hash.get("server") || params.get("server") || hash.get("g2_server") || params.get("g2_server") || "").trim();
105
+ const token = (hash.get("token") || params.get("token") || hash.get("g2_token") || params.get("g2_token") || "").trim();
106
+ if (server) localStorage.setItem(STORAGE_SERVER, server);
107
+ if (token) localStorage.setItem(STORAGE_TOKEN, token);
108
+ if (server || token) {
109
+ appendLog("Configured from QR.");
110
+ setStatus("Configured from QR");
111
+ if (window.location.hash) {
112
+ history.replaceState(null, "", window.location.pathname + window.location.search);
113
+ }
114
+ }
115
+ // Fall back to build-time embedded values when localStorage is empty.
116
+ if (!localStorage.getItem(STORAGE_SERVER) && EMBEDDED_SERVER) {
117
+ localStorage.setItem(STORAGE_SERVER, EMBEDDED_SERVER);
118
+ }
119
+ if (!localStorage.getItem(STORAGE_TOKEN) && EMBEDDED_TOKEN) {
120
+ localStorage.setItem(STORAGE_TOKEN, EMBEDDED_TOKEN);
121
+ }
122
+ return { server, token };
123
+ }
124
+
125
+ async function waitForBridge(timeoutMs: number): Promise<EvenAppBridge | null> {
126
+ try {
127
+ return await Promise.race([
128
+ waitForEvenAppBridge(),
129
+ new Promise<null>((resolve) => setTimeout(() => resolve(null), timeoutMs)),
130
+ ]);
131
+ } catch {
132
+ return null;
133
+ }
134
+ }
135
+
136
+ async function saveSettings(): Promise<void> {
137
+ await writeSetting(STORAGE_SERVER, serverInput.value.trim());
138
+ await writeSetting(STORAGE_TOKEN, tokenInput.value.trim());
139
+ connect();
140
+ setStatus("Saved");
141
+ }
142
+
143
+ async function startGlassesMode(): Promise<void> {
144
+ if (!bridge) return;
145
+ const server = await readSetting(STORAGE_SERVER);
146
+ if (!server) {
147
+ await createOrUpdateGlasses("Open the phone app and set server URL.");
148
+ return;
149
+ }
150
+ connect();
151
+ await renderGlasses("Agent ready");
152
+ }
153
+
154
+ async function createOrUpdateGlasses(text: string): Promise<void> {
155
+ if (!bridge) return;
156
+ if (!glassesReady) {
157
+ const result = await bridge.createStartUpPageContainer({
158
+ containerTotalNum: 1,
159
+ textObject: [
160
+ {
161
+ xPosition: 8,
162
+ yPosition: 0,
163
+ width: 560,
164
+ height: 288,
165
+ paddingLength: 4,
166
+ containerID: 1,
167
+ containerName: "status",
168
+ content: text,
169
+ isEventCapture: 1,
170
+ },
171
+ ],
172
+ });
173
+ glassesReady = result === StartUpPageCreateResult.success || result === 0;
174
+ if (!glassesReady) {
175
+ setStatus(`Glasses UI failed: ${result}`);
176
+ return;
177
+ }
178
+ }
179
+ const content = text.slice(0, 1800);
180
+ await bridge.textContainerUpgrade({
181
+ containerID: 1,
182
+ containerName: "status",
183
+ contentOffset: 0,
184
+ contentLength: content.length,
185
+ content,
186
+ });
187
+ }
188
+
189
+ async function handleEvenHubEvent(event: EvenHubEvent): Promise<void> {
190
+ try {
191
+ if (event.audioEvent) {
192
+ if (recording) {
193
+ audioChunks.push(event.audioEvent.audioPcm);
194
+ }
195
+ return;
196
+ }
197
+
198
+ let eventType: OsEventTypeList | undefined;
199
+ try {
200
+ eventType = normalizeEventType(rawEventType(event));
201
+ } catch (error) {
202
+ appendLog(`G2 normalize error: ${safeError(error)}`);
203
+ eventType = undefined;
204
+ }
205
+
206
+ if (eventType === OsEventTypeList.SCROLL_TOP_EVENT) {
207
+ moveSelection(-1);
208
+ await renderGlasses(glassesStatusText);
209
+ return;
210
+ }
211
+ if (eventType === OsEventTypeList.SCROLL_BOTTOM_EVENT) {
212
+ moveSelection(1);
213
+ await renderGlasses(glassesStatusText);
214
+ return;
215
+ }
216
+ if (eventType === OsEventTypeList.DOUBLE_CLICK_EVENT) {
217
+ if (recording) {
218
+ await toggleRecording();
219
+ } else {
220
+ await shutdown();
221
+ }
222
+ return;
223
+ }
224
+ if (isClickEvent(event, eventType)) {
225
+ const action = selectedActionFromEvent(event);
226
+ await runAction(action);
227
+ return;
228
+ }
229
+ const sys = event.sysEvent;
230
+ if (
231
+ sys?.eventType === OsEventTypeList.ABNORMAL_EXIT_EVENT ||
232
+ sys?.eventType === OsEventTypeList.SYSTEM_EXIT_EVENT
233
+ ) {
234
+ await stopAudio();
235
+ }
236
+ } catch (error) {
237
+ appendLog(`G2 handler error: ${safeError(error)}`);
238
+ }
239
+ }
240
+
241
+ function safeError(error: unknown): string {
242
+ if (error instanceof Error) return `${error.name}: ${error.message}`;
243
+ try { return String(error); } catch { return "(unknown error)"; }
244
+ }
245
+
246
+ async function renderGlasses(text: string): Promise<void> {
247
+ glassesStatusText = text;
248
+ try {
249
+ await createOrUpdateGlasses(formatGlassesText(text));
250
+ } catch (error) {
251
+ appendLog(`renderGlasses error: ${safeError(error)}`);
252
+ }
253
+ }
254
+
255
+ function formatGlassesText(text: string): string {
256
+ const actions = currentMenu();
257
+ const menu = actions
258
+ .map((action, index) => {
259
+ const marker = index === selectedActionIndex ? ">" : " ";
260
+ return `${marker} ${action}`;
261
+ })
262
+ .join("\n");
263
+ return `${text}\n\n${menu}`;
264
+ }
265
+
266
+ function rawEventType(event: EvenHubEvent): unknown {
267
+ const loose = event as Record<string, any>;
268
+ const candidates = [
269
+ event.listEvent?.eventType,
270
+ event.textEvent?.eventType,
271
+ event.sysEvent?.eventType,
272
+ (event.listEvent as any)?.Event_Type,
273
+ (event.textEvent as any)?.Event_Type,
274
+ (event.sysEvent as any)?.Event_Type,
275
+ loose.eventType,
276
+ loose.Event_Type,
277
+ loose.type,
278
+ loose.data?.eventType,
279
+ loose.data?.Event_Type,
280
+ loose.jsonData?.eventType,
281
+ loose.jsonData?.Event_Type,
282
+ ];
283
+ for (const candidate of candidates) {
284
+ if (candidate !== undefined && candidate !== null && candidate !== "") return candidate;
285
+ }
286
+ return undefined;
287
+ }
288
+
289
+ function normalizeEventType(raw: unknown): OsEventTypeList | undefined {
290
+ if (typeof raw === "number") {
291
+ return Number.isInteger(raw) && raw >= 0 && raw <= 8 ? raw as OsEventTypeList : undefined;
292
+ }
293
+ if (typeof raw !== "string") return undefined;
294
+ const key = raw.split(".").pop()?.trim().toUpperCase().replace(/^OS_EVENT_TYPE_LIST_/, "") || "";
295
+ if (key === "CLICK") return OsEventTypeList.CLICK_EVENT;
296
+ if (key === "SCROLL_TOP") return OsEventTypeList.SCROLL_TOP_EVENT;
297
+ if (key === "SCROLL_BOTTOM") return OsEventTypeList.SCROLL_BOTTOM_EVENT;
298
+ if (key === "DOUBLE_CLICK") return OsEventTypeList.DOUBLE_CLICK_EVENT;
299
+ if (key === "ABNORMAL_EXIT") return OsEventTypeList.ABNORMAL_EXIT_EVENT;
300
+ if (key === "SYSTEM_EXIT") return OsEventTypeList.SYSTEM_EXIT_EVENT;
301
+ const value = (OsEventTypeList as Record<string, number | string>)[key];
302
+ return typeof value === "number" ? value as OsEventTypeList : undefined;
303
+ }
304
+
305
+ function isClickEvent(event: EvenHubEvent, eventType: OsEventTypeList | undefined): boolean {
306
+ if (eventType === OsEventTypeList.CLICK_EVENT) return true;
307
+ if (eventType !== undefined) return false;
308
+ if (event.listEvent || event.textEvent) return true;
309
+ const loose = event as Record<string, any>;
310
+ const eventSourceCandidates = [
311
+ event.sysEvent && (event.sysEvent as any).eventSource,
312
+ loose.eventSource,
313
+ loose.jsonData?.eventSource,
314
+ loose.jsondata?.eventSource,
315
+ ];
316
+ return eventSourceCandidates.some((value) => typeof value === "number" && value > 0);
317
+ }
318
+
319
+ function moveSelection(delta: number): void {
320
+ const actions = currentMenu();
321
+ selectedActionIndex = (selectedActionIndex + delta + actions.length) % actions.length;
322
+ appendLog(`Selected: ${actions[selectedActionIndex]}`);
323
+ }
324
+
325
+ function selectedActionFromEvent(event: EvenHubEvent): ActionName {
326
+ const item = event.listEvent?.currentSelectItemName?.trim();
327
+ if (isActionName(item)) return item;
328
+ const actions = currentMenu();
329
+ const index = event.listEvent?.currentSelectItemIndex;
330
+ if (typeof index === "number" && index >= 0 && index < actions.length) {
331
+ selectedActionIndex = index;
332
+ }
333
+ return actions[selectedActionIndex];
334
+ }
335
+
336
+ function isActionName(value: string | undefined): value is ActionName {
337
+ return value === "Talk" || value === "Send" || value === "Cancel";
338
+ }
339
+
340
+ async function runAction(action: ActionName): Promise<void> {
341
+ appendLog(`Action: ${action}`);
342
+ if (action === "Talk" || action === "Send") {
343
+ await toggleRecording();
344
+ } else if (action === "Cancel") {
345
+ if (recording) {
346
+ await cancelRecording();
347
+ } else if (thinking) {
348
+ await cancelThinking();
349
+ }
350
+ }
351
+ }
352
+
353
+ async function cancelRecording(): Promise<void> {
354
+ if (!recording) return;
355
+ await stopAudio();
356
+ audioChunks = [];
357
+ selectedActionIndex = 0;
358
+ await renderGlasses("Cancelled");
359
+ }
360
+
361
+ async function cancelThinking(): Promise<void> {
362
+ if (!thinking) return;
363
+ thinking = false;
364
+ ignoreNextReply = true;
365
+ pendingTranscript = null;
366
+ selectedActionIndex = 0;
367
+ await renderGlasses("Cancelled");
368
+ }
369
+
370
+ async function toggleRecording(): Promise<void> {
371
+ if (!bridge) {
372
+ appendLog("toggleRecording: no bridge");
373
+ return;
374
+ }
375
+ if (!recording) {
376
+ audioChunks = [];
377
+ recording = true;
378
+ selectedActionIndex = 0;
379
+ let opened: unknown;
380
+ try {
381
+ opened = await bridge.audioControl(true);
382
+ } catch (error) {
383
+ appendLog(`audioControl(true) threw: ${safeError(error)}`);
384
+ opened = false;
385
+ }
386
+ appendLog(`audioControl(true) -> ${opened}`);
387
+ if (!opened) {
388
+ recording = false;
389
+ await renderGlasses("Mic failed");
390
+ return;
391
+ }
392
+ await renderGlasses("Listening...");
393
+ return;
394
+ }
395
+ await stopAudio();
396
+ selectedActionIndex = 0;
397
+ if (audioChunks.length === 0) {
398
+ await renderGlasses("No audio captured");
399
+ return;
400
+ }
401
+ const pcmBase64 = bytesToBase64(concatBytes(audioChunks));
402
+ const sent = sendJson({ type: "audio", pcmBase64, sample_rate: 16000, channels: 1, input_mode: "voice" });
403
+ if (!sent) {
404
+ thinking = false;
405
+ await renderGlasses("Not connected.\nCheck server URL and token.");
406
+ return;
407
+ }
408
+ thinking = true;
409
+ await renderGlasses("Thinking...");
410
+ }
411
+
412
+ async function stopAudio(): Promise<void> {
413
+ if (!bridge || !recording) return;
414
+ recording = false;
415
+ await bridge.audioControl(false);
416
+ }
417
+
418
+ async function shutdown(): Promise<void> {
419
+ await stopAudio();
420
+ if (bridge) {
421
+ await bridge.shutDownPageContainer(1);
422
+ }
423
+ glassesReady = false;
424
+ }
425
+
426
+ function connect(): void {
427
+ void connectAsync();
428
+ }
429
+
430
+ async function connectAsync(): Promise<void> {
431
+ const server = await readSetting(STORAGE_SERVER);
432
+ if (!server) {
433
+ setStatus("Set server URL");
434
+ return;
435
+ }
436
+ const token = await readSetting(STORAGE_TOKEN);
437
+ const url = new URL("/ws", server);
438
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
439
+ if (token) url.searchParams.set("token", token);
440
+ ws?.close();
441
+ ws = new WebSocket(url);
442
+ ws.addEventListener("open", () => setStatus("Connected"));
443
+ ws.addEventListener("close", () => setStatus("Disconnected"));
444
+ ws.addEventListener("message", (event) => {
445
+ void handleGatewayMessage(JSON.parse(event.data));
446
+ });
447
+ }
448
+
449
+ async function handleGatewayMessage(message: any): Promise<void> {
450
+ if (message.type === "reply") {
451
+ const text = message.display || message.reply || "";
452
+ appendLog(`Agent: ${text}`);
453
+ pendingTranscript = null;
454
+ thinking = false;
455
+ if (ignoreNextReply) {
456
+ ignoreNextReply = false;
457
+ return;
458
+ }
459
+ selectedActionIndex = 0;
460
+ await renderGlasses(text || "Done");
461
+ } else if (message.type === "notification") {
462
+ const notification = message.notification;
463
+ pendingTranscript = null;
464
+ thinking = false;
465
+ selectedActionIndex = 0;
466
+ await renderGlasses(`${notification.title}\n${notification.body}`);
467
+ } else if (message.type === "thinking" || message.type === "transcribing") {
468
+ if (!thinking) return;
469
+ await renderGlasses(pendingTranscript ? `You: ${pendingTranscript}\n\nThinking...` : "Thinking...");
470
+ } else if (message.type === "transcript") {
471
+ appendLog(`G2 voice: ${message.text}`);
472
+ if (!thinking) return;
473
+ if (typeof message.text === "string" && message.text.trim()) {
474
+ pendingTranscript = message.text.trim();
475
+ await renderGlasses(`You: ${pendingTranscript}\n\nThinking...`);
476
+ }
477
+ } else if (message.type === "error") {
478
+ pendingTranscript = null;
479
+ thinking = false;
480
+ if (ignoreNextReply) {
481
+ ignoreNextReply = false;
482
+ return;
483
+ }
484
+ selectedActionIndex = 0;
485
+ await renderGlasses(`Error\n${message.error}`);
486
+ }
487
+ }
488
+
489
+ function sendJson(payload: Record<string, unknown>): boolean {
490
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
491
+ appendLog("Gateway is not connected.");
492
+ return false;
493
+ }
494
+ ws.send(JSON.stringify(payload));
495
+ return true;
496
+ }
497
+
498
+ async function readSetting(key: string): Promise<string> {
499
+ if (bridge) {
500
+ const value = await bridge.getLocalStorage(key);
501
+ if (value) return value;
502
+ }
503
+ return localStorage.getItem(key) || "";
504
+ }
505
+
506
+ async function writeSetting(key: string, value: string): Promise<void> {
507
+ if (bridge) {
508
+ await bridge.setLocalStorage(key, value);
509
+ }
510
+ localStorage.setItem(key, value);
511
+ }
512
+
513
+ function concatBytes(chunks: Uint8Array[]): Uint8Array {
514
+ const total = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
515
+ const output = new Uint8Array(total);
516
+ let offset = 0;
517
+ for (const chunk of chunks) {
518
+ output.set(chunk, offset);
519
+ offset += chunk.length;
520
+ }
521
+ return output;
522
+ }
523
+
524
+ function bytesToBase64(bytes: Uint8Array): string {
525
+ let binary = "";
526
+ const step = 0x8000;
527
+ for (let index = 0; index < bytes.length; index += step) {
528
+ binary += String.fromCharCode(...bytes.subarray(index, index + step));
529
+ }
530
+ return btoa(binary);
531
+ }
532
+
533
+ function setStatus(text: string): void {
534
+ statusEl.textContent = text;
535
+ }
536
+
537
+ function appendLog(text: string): void {
538
+ logEl.textContent = `${logEl.textContent}\n${text}`.trim();
539
+ }
@@ -0,0 +1,70 @@
1
+ :root {
2
+ color-scheme: dark;
3
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
4
+ background: #101412;
5
+ color: #eef6ef;
6
+ }
7
+
8
+ body {
9
+ margin: 0;
10
+ }
11
+
12
+ main {
13
+ width: min(720px, calc(100vw - 32px));
14
+ margin: 0 auto;
15
+ padding: 28px 0;
16
+ }
17
+
18
+ h1 {
19
+ margin: 0 0 18px;
20
+ font-size: 24px;
21
+ line-height: 1.2;
22
+ }
23
+
24
+ form {
25
+ display: grid;
26
+ gap: 10px;
27
+ margin: 12px 0;
28
+ }
29
+
30
+ label {
31
+ display: grid;
32
+ gap: 5px;
33
+ color: #b6c7bd;
34
+ font-size: 13px;
35
+ }
36
+
37
+ input,
38
+ button {
39
+ min-height: 42px;
40
+ box-sizing: border-box;
41
+ border: 1px solid #405048;
42
+ border-radius: 6px;
43
+ padding: 9px 11px;
44
+ font: inherit;
45
+ }
46
+
47
+ input {
48
+ width: 100%;
49
+ background: #0e1310;
50
+ color: #eef6ef;
51
+ }
52
+
53
+ button {
54
+ background: #d7f7d8;
55
+ color: #102014;
56
+ }
57
+
58
+ #status {
59
+ color: #b6c7bd;
60
+ }
61
+
62
+ #log {
63
+ min-height: 180px;
64
+ padding: 12px;
65
+ border: 1px solid #38443d;
66
+ border-radius: 8px;
67
+ background: #161d19;
68
+ white-space: pre-wrap;
69
+ overflow-wrap: anywhere;
70
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "strict": true,
7
+ "skipLibCheck": true,
8
+ "types": ["vite/client"]
9
+ },
10
+ "include": ["src/**/*.ts"]
11
+ }
@@ -5,19 +5,37 @@ The entry point is run(ctx, input). Runtime provides:
5
5
  ctx.log.info(msg) / warn / error : writes to logs/runs/<run-id>.json and events.jsonl
6
6
  ctx.memory.get(key) / set(key, v) : persists under skill.yaml memory.namespace
7
7
  ctx.ai.run(step_id, payload) : calls an ai_steps entry declared in skill.yaml
8
- ctx.notify(args) : notifies Discord/Telegram/macOS/Mail/Slack/stderr (auto recommended)
8
+ ctx.notify(args) : notifies Discord/Telegram/macOS/Mail/Slack/stderr (auto recommended);
9
+ pass filePath/filePaths to attach local files on Discord/Telegram
10
+ ctx.history.append({time?, meta?, content, replace?}) : insert/upsert one row in this skill's SQLite log
11
+ ctx.history.list({from?, to?, limit?}) : read past rows as [{time, meta, content}] (date range filter)
12
+ ctx.history.read({from?, to?}) : read past rows as one concatenated string (handy for ai_steps)
9
13
  ctx.now() : returns the current time as an ISO8601 string
10
14
 
11
15
  input format:
12
16
  {
13
17
  "args": arguments validated by skill.yaml input.schema,
14
18
  "trigger": {"type": "manual", "id": ..., "time": "ISO8601"},
15
- "sources": {"workspace": ..., "notes_dir": ..., ...},
19
+ "sources": {"workspace": ..., "notes_dir": ..., "skill_output_dir": ..., ...},
16
20
  "memory": current ctx.memory snapshot,
17
21
  }
18
22
 
19
23
  Return a SkillResult. outputs.<id> maps to outputs[id] in skill.yaml.
20
24
  Runtime saves outputs, so the skill should not write files directly.
25
+
26
+ If you need to write a user-visible markdown file with a name the runtime's
27
+ outputs block doesn't fit (free-form filename, rotating log, accumulating list),
28
+ write it under input["sources"]["skill_output_dir"]:
29
+
30
+ out_dir = input["sources"]["skill_output_dir"]
31
+ with open(os.path.join(out_dir, "my-log.md"), "a") as f:
32
+ f.write("...\n")
33
+
34
+ The runtime creates this dir before invoking the skill. Use a plain filename
35
+ like "marketing-log.md" — DO NOT create date-based subdirectories like
36
+ 2026/05/2026-05-22.md. Files placed here surface in Obsidian as
37
+ "06 Skills/<skill-id>/...". For internal state the user shouldn't see, use
38
+ ctx.memory or ctx.history, not this dir.
21
39
  """
22
40
 
23
41
  from __future__ import annotations
@@ -42,6 +42,15 @@ memory:
42
42
  read: true
43
43
  write: true
44
44
 
45
+ # Enable history when the skill should accumulate a queryable log (food,
46
+ # weight, training, reviews, etc.). Entries land in ~/.agent-sin/data/<skill-id>.db
47
+ # (SQLite, hidden from the user) and the skill reads them back via
48
+ # ctx.history.list({from, to, limit}). Pair it with an outputs entry below if
49
+ # you also want a human-readable Markdown report under notes/.
50
+ # history:
51
+ # read: true
52
+ # write: true
53
+
45
54
  # Optional: set "raw" only when you want to return the summary directly to users.
46
55
  # By default, the LLM receives the result and formats a natural-language reply.
47
56
  # See https://agent.shingoirie.com/skill-authoring for details.