agent-sin 0.1.12 → 0.1.15

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 +66 -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 +568 -14
  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
@@ -30,6 +30,10 @@ export interface CodexAppServerOptions {
30
30
  turnTimeoutMs?: number;
31
31
  onStderr?: (chunk: string) => void;
32
32
  }
33
+ export interface CodexTurnResult {
34
+ text: string;
35
+ generatedImagePaths: string[];
36
+ }
33
37
  export declare function getSharedCodexAppServer(model?: string): CodexAppServerSession;
34
38
  export declare function shutdownSharedCodexAppServer(): Promise<void>;
35
39
  export declare class CodexAppServerSession {
@@ -42,7 +46,7 @@ export declare class CodexAppServerSession {
42
46
  private readonly options;
43
47
  private exitReason;
44
48
  constructor(options?: CodexAppServerOptions);
45
- sendTurn(text: string, options?: CodexTurnOptions): Promise<string>;
49
+ sendTurn(text: string, options?: CodexTurnOptions): Promise<CodexTurnResult>;
46
50
  stop(): Promise<void>;
47
51
  isRunning(): boolean;
48
52
  private ensureStarted;
@@ -1,7 +1,13 @@
1
1
  import { spawn } from "node:child_process";
2
+ import crypto from "node:crypto";
3
+ import { mkdir, stat, writeFile } from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { detectImageMimeTypeFromBuffer, imageExtensionForMimeType, } from "../core/image-attachments.js";
2
7
  import { l } from "../core/i18n.js";
3
8
  const DEFAULT_STARTUP_TIMEOUT_MS = 30_000;
4
9
  const DEFAULT_TURN_TIMEOUT_MS = 5 * 60_000;
10
+ const MAX_GENERATED_IMAGE_BYTES = 20 * 1024 * 1024;
5
11
  function snippet(value) {
6
12
  if (typeof value !== "string") {
7
13
  return undefined;
@@ -40,8 +46,8 @@ export class CodexAppServerSession {
40
46
  constructor(options = {}) {
41
47
  const baseArgs = options.args ? [...options.args] : ["app-server"];
42
48
  const model = options.model || process.env.AGENT_SIN_CODEX_MODEL;
43
- if (model && !baseArgs.includes("--model") && !baseArgs.includes("-m")) {
44
- baseArgs.push("--model", model);
49
+ if (model && !hasConfigOverride(baseArgs, "model")) {
50
+ baseArgs.push("-c", `model=${JSON.stringify(model)}`);
45
51
  }
46
52
  this.options = {
47
53
  bin: options.bin || process.env.AGENT_SIN_CODEX_BIN || "codex",
@@ -211,6 +217,9 @@ export class CodexAppServerSession {
211
217
  return new Promise((resolve, reject) => {
212
218
  let assistantText = "";
213
219
  let settled = false;
220
+ const generatedImagePaths = [];
221
+ const generatedImageKeys = new Set();
222
+ const generatedImageTasks = [];
214
223
  const emit = (event) => {
215
224
  if (!options.onProgress) {
216
225
  return;
@@ -259,6 +268,17 @@ export class CodexAppServerSession {
259
268
  else if (item && (item.type === "command" || item.type === "tool_call")) {
260
269
  emit({ kind: "tool", name: item.name || item.type, text: l("done", "完了") });
261
270
  }
271
+ else if (item && item.type === "imageGeneration") {
272
+ generatedImageTasks.push(captureGeneratedImage(item, options.cwd, generatedImagePaths, generatedImageKeys));
273
+ emit({ kind: "tool", name: "image_generation", text: l("done", "完了") });
274
+ }
275
+ }
276
+ else if (method === "rawResponseItem/completed") {
277
+ const item = params.item;
278
+ if (item && item.type === "image_generation_call") {
279
+ generatedImageTasks.push(captureGeneratedImage(item, options.cwd, generatedImagePaths, generatedImageKeys));
280
+ emit({ kind: "tool", name: "image_generation", text: l("done", "完了") });
281
+ }
262
282
  }
263
283
  else if (method === "turn/completed") {
264
284
  const turn = params.turn;
@@ -270,6 +290,11 @@ export class CodexAppServerSession {
270
290
  if (fromTurn) {
271
291
  assistantText = fromTurn;
272
292
  }
293
+ for (const entry of turn.items) {
294
+ if (entry && entry.type === "imageGeneration") {
295
+ generatedImageTasks.push(captureGeneratedImage(entry, options.cwd, generatedImagePaths, generatedImageKeys));
296
+ }
297
+ }
273
298
  }
274
299
  finish(true, undefined);
275
300
  }
@@ -285,12 +310,23 @@ export class CodexAppServerSession {
285
310
  settled = true;
286
311
  clearTimeout(timeout);
287
312
  this.notificationHandlers.delete(handler);
288
- if (ok) {
289
- resolve(assistantText);
290
- }
291
- else {
292
- reject(error || new Error("codex app-server: unknown failure"));
293
- }
313
+ Promise.all(generatedImageTasks)
314
+ .then(() => {
315
+ if (ok) {
316
+ resolve({ text: assistantText, generatedImagePaths });
317
+ }
318
+ else {
319
+ reject(error || new Error("codex app-server: unknown failure"));
320
+ }
321
+ })
322
+ .catch((imageError) => {
323
+ if (ok) {
324
+ resolve({ text: assistantText, generatedImagePaths });
325
+ }
326
+ else {
327
+ reject(error || imageError);
328
+ }
329
+ });
294
330
  };
295
331
  const timeout = setTimeout(() => {
296
332
  finish(false, new Error(`codex app-server: turn timed out after ${this.options.turnTimeoutMs}ms`));
@@ -306,3 +342,106 @@ export class CodexAppServerSession {
306
342
  });
307
343
  }
308
344
  }
345
+ function hasConfigOverride(args, key) {
346
+ for (let index = 0; index < args.length; index += 1) {
347
+ const arg = args[index];
348
+ if ((arg === "-c" || arg === "--config") && args[index + 1]?.startsWith(`${key}=`)) {
349
+ return true;
350
+ }
351
+ if (arg.startsWith("-c") && arg.slice(2).trimStart().startsWith(`${key}=`)) {
352
+ return true;
353
+ }
354
+ if (arg.startsWith("--config=") && arg.slice("--config=".length).startsWith(`${key}=`)) {
355
+ return true;
356
+ }
357
+ }
358
+ return false;
359
+ }
360
+ async function captureGeneratedImage(item, cwd, paths, keys) {
361
+ if (!item || typeof item !== "object")
362
+ return;
363
+ const record = item;
364
+ const id = typeof record.id === "string" ? record.id : undefined;
365
+ const savedPath = typeof record.savedPath === "string" ? record.savedPath : undefined;
366
+ const result = typeof record.result === "string" ? record.result : undefined;
367
+ const key = id || (result ? imageResultKey(result) : savedPath);
368
+ if (key && keys.has(key))
369
+ return;
370
+ if (key)
371
+ keys.add(key);
372
+ if (savedPath) {
373
+ const usable = await existingImagePath(savedPath);
374
+ if (usable && !paths.includes(usable)) {
375
+ paths.push(usable);
376
+ return;
377
+ }
378
+ }
379
+ if (!result)
380
+ return;
381
+ const resultPath = await persistImageGenerationResult(result, cwd, id);
382
+ if (resultPath && !paths.includes(resultPath)) {
383
+ paths.push(resultPath);
384
+ }
385
+ }
386
+ async function existingImagePath(filePath) {
387
+ try {
388
+ const info = await stat(filePath);
389
+ return info.isFile() && info.size > 0 ? filePath : null;
390
+ }
391
+ catch {
392
+ return null;
393
+ }
394
+ }
395
+ async function persistImageGenerationResult(value, cwd, id) {
396
+ const parsed = parseImageGenerationResult(value);
397
+ if (!parsed || parsed.buffer.length === 0 || parsed.buffer.length > MAX_GENERATED_IMAGE_BYTES) {
398
+ return null;
399
+ }
400
+ const mimeType = parsed.mimeType || detectImageMimeTypeFromBuffer(parsed.buffer);
401
+ const ext = imageExtensionForMimeType(mimeType);
402
+ const safeId = (id || "image").replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 40) || "image";
403
+ const hash = crypto.createHash("sha256").update(parsed.buffer).digest("hex").slice(0, 10);
404
+ const filename = `${new Date().toISOString().replace(/[:.]/g, "-")}-${safeId}-${hash}${ext}`;
405
+ for (const dir of candidateImageDirs(cwd)) {
406
+ try {
407
+ await mkdir(dir, { recursive: true });
408
+ const filePath = path.join(dir, filename);
409
+ await writeFile(filePath, parsed.buffer);
410
+ return filePath;
411
+ }
412
+ catch {
413
+ // Try the fallback directory.
414
+ }
415
+ }
416
+ return null;
417
+ }
418
+ function parseImageGenerationResult(value) {
419
+ const trimmed = value.trim();
420
+ const dataUrl = trimmed.match(/^data:(image\/[a-z0-9.+-]+);base64,([\s\S]+)$/i);
421
+ const mimeType = dataUrl?.[1];
422
+ const rawBase64 = dataUrl ? dataUrl[2] : trimmed;
423
+ if (rawBase64.length === 0 || rawBase64.length > Math.ceil(MAX_GENERATED_IMAGE_BYTES * 1.4)) {
424
+ return null;
425
+ }
426
+ if (!/^[A-Za-z0-9+/=\s_-]+$/.test(rawBase64)) {
427
+ return null;
428
+ }
429
+ const normalized = rawBase64.replace(/[\s_-]/g, (char) => (char === "-" ? "+" : char === "_" ? "/" : ""));
430
+ try {
431
+ return { buffer: Buffer.from(normalized, "base64"), mimeType };
432
+ }
433
+ catch {
434
+ return null;
435
+ }
436
+ }
437
+ function candidateImageDirs(cwd) {
438
+ const dirs = [];
439
+ if (cwd) {
440
+ dirs.push(path.join(cwd, "codex-images"));
441
+ }
442
+ dirs.push(path.join(os.tmpdir(), "agent-sin-codex-images"));
443
+ return dirs;
444
+ }
445
+ function imageResultKey(value) {
446
+ return crypto.createHash("sha256").update(value).digest("hex");
447
+ }
@@ -4,10 +4,13 @@ import path from "node:path";
4
4
  import { resolveSkillEntryPath } from "../core/skill-registry.js";
5
5
  import { getAiProvider } from "../core/ai-provider.js";
6
6
  import { notify as runNotify } from "../core/notifier.js";
7
+ import { appendSkillHistory, listSkillHistory, readSkillHistoryRaw, } from "../core/skill-history.js";
7
8
  const AI_REQUEST_PREFIX = "AGENT_SIN_AI_REQUEST::";
8
9
  const AI_RESPONSE_PREFIX = "AGENT_SIN_AI_RESPONSE::";
9
10
  const NOTIFY_REQUEST_PREFIX = "AGENT_SIN_NOTIFY_REQUEST::";
10
11
  const NOTIFY_RESPONSE_PREFIX = "AGENT_SIN_NOTIFY_RESPONSE::";
12
+ const HISTORY_REQUEST_PREFIX = "AGENT_SIN_HISTORY_REQUEST::";
13
+ const HISTORY_RESPONSE_PREFIX = "AGENT_SIN_HISTORY_RESPONSE::";
11
14
  const CTX_LOG_PATTERN = /^\[(info|warn|error)\]\s+([\s\S]*)$/;
12
15
  export function candidatePythonInterpreters(config, platform = process.platform) {
13
16
  const venvDir = path.join(config.workspace, ".venv");
@@ -83,6 +86,13 @@ export async function runPythonSkill(config, manifest, input) {
83
86
  });
84
87
  return;
85
88
  }
89
+ if (line.startsWith(HISTORY_REQUEST_PREFIX)) {
90
+ const payloadJson = line.slice(HISTORY_REQUEST_PREFIX.length);
91
+ handleHistoryRequest(payloadJson).catch((error) => {
92
+ stderrLines.push(`[history-error] ${error instanceof Error ? error.message : String(error)}`);
93
+ });
94
+ return;
95
+ }
86
96
  const ctxMatch = line.match(CTX_LOG_PATTERN);
87
97
  if (ctxMatch) {
88
98
  ctxLogs.push({ level: ctxMatch[1], message: ctxMatch[2] });
@@ -103,6 +113,7 @@ export async function runPythonSkill(config, manifest, input) {
103
113
  const response = await getAiProvider()(config, {
104
114
  model_id: request.model_id,
105
115
  messages: request.messages,
116
+ cwd: config.workspace,
106
117
  });
107
118
  child.stdin.write(`${AI_RESPONSE_PREFIX}${JSON.stringify({ id: request.id, ok: true, response })}\n`);
108
119
  }
@@ -114,6 +125,40 @@ export async function runPythonSkill(config, manifest, input) {
114
125
  })}\n`);
115
126
  }
116
127
  }
128
+ async function handleHistoryRequest(payloadJson) {
129
+ let request;
130
+ try {
131
+ request = JSON.parse(payloadJson);
132
+ }
133
+ catch (error) {
134
+ child.stdin.write(`${HISTORY_RESPONSE_PREFIX}${JSON.stringify({ id: "?", ok: false, error: `Invalid history request JSON: ${String(error)}` })}\n`);
135
+ return;
136
+ }
137
+ try {
138
+ const payload = request.payload || {};
139
+ let data = null;
140
+ if (request.op === "append") {
141
+ data = await appendSkillHistory(config, manifest, payload);
142
+ }
143
+ else if (request.op === "list") {
144
+ data = await listSkillHistory(config, manifest, payload);
145
+ }
146
+ else if (request.op === "read") {
147
+ data = await readSkillHistoryRaw(config, manifest, payload);
148
+ }
149
+ else {
150
+ throw new Error(`Unknown history op: ${request.op}`);
151
+ }
152
+ child.stdin.write(`${HISTORY_RESPONSE_PREFIX}${JSON.stringify({ id: request.id, ok: true, data })}\n`);
153
+ }
154
+ catch (error) {
155
+ child.stdin.write(`${HISTORY_RESPONSE_PREFIX}${JSON.stringify({
156
+ id: request.id,
157
+ ok: false,
158
+ error: error instanceof Error ? error.message : String(error),
159
+ })}\n`);
160
+ }
161
+ }
117
162
  async function handleNotifyRequest(payloadJson) {
118
163
  let request;
119
164
  try {
@@ -134,6 +179,15 @@ export async function runPythonSkill(config, manifest, input) {
134
179
  to: typeof args.to === "string" ? args.to : undefined,
135
180
  discordThreadId: typeof args.discordThreadId === "string" ? args.discordThreadId : undefined,
136
181
  telegramThreadId: typeof args.telegramThreadId === "string" ? args.telegramThreadId : undefined,
182
+ filePath: typeof args.filePath === "string" ? args.filePath : undefined,
183
+ filePaths: Array.isArray(args.filePaths)
184
+ ? args.filePaths.filter((item) => typeof item === "string")
185
+ : undefined,
186
+ imagePath: typeof args.imagePath === "string" ? args.imagePath : undefined,
187
+ imagePaths: Array.isArray(args.imagePaths)
188
+ ? args.imagePaths.filter((item) => typeof item === "string")
189
+ : undefined,
190
+ cwd: config.workspace,
137
191
  });
138
192
  child.stdin.write(`${NOTIFY_RESPONSE_PREFIX}${JSON.stringify({ id: request.id, ok: result.ok, channel: result.channel, detail: result.detail })}\n`);
139
193
  }
@@ -238,6 +292,7 @@ class AI:
238
292
  "model_id": response.get("model_id"),
239
293
  "provider": response.get("provider"),
240
294
  "text": response.get("text", ""),
295
+ "generated_images": response.get("generated_images") or [],
241
296
  }
242
297
 
243
298
  class Memory:
@@ -275,11 +330,38 @@ async def notify_call(args):
275
330
  "detail": envelope.get("detail"),
276
331
  }
277
332
 
333
+ async def history_call(op, payload):
334
+ request_id = str(uuid.uuid4())
335
+ request = {"id": request_id, "op": op, "payload": payload or {}}
336
+ print("AGENT_SIN_HISTORY_REQUEST::" + json.dumps(request, ensure_ascii=False, default=str), file=sys.stderr, flush=True)
337
+ line = sys.stdin.readline()
338
+ if not line:
339
+ raise RuntimeError("History channel closed")
340
+ marker = "AGENT_SIN_HISTORY_RESPONSE::"
341
+ idx = line.find(marker)
342
+ if idx < 0:
343
+ raise RuntimeError(f"Unexpected history response: {line.strip()}")
344
+ envelope = json.loads(line[idx + len(marker):])
345
+ if not envelope.get("ok"):
346
+ raise RuntimeError(envelope.get("error") or "History error")
347
+ return envelope.get("data")
348
+
349
+ class History:
350
+ async def append(self, args):
351
+ if not isinstance(args, dict):
352
+ raise RuntimeError("ctx.history.append requires a dict argument")
353
+ return await history_call("append", args)
354
+ async def list(self, options=None):
355
+ return await history_call("list", options or {})
356
+ async def read(self, options=None):
357
+ return await history_call("read", options or {})
358
+
278
359
  class Ctx:
279
360
  def __init__(self):
280
361
  self.log = Log()
281
362
  self.ai = AI()
282
363
  self.memory = Memory()
364
+ self.history = History()
283
365
  def now(self):
284
366
  return datetime.now(timezone.utc).isoformat()
285
367
  async def notify(self, args):
@@ -7,6 +7,7 @@ import ts from "typescript";
7
7
  import { resolveSkillEntryPath } from "../core/skill-registry.js";
8
8
  import { getAiProvider } from "../core/ai-provider.js";
9
9
  import { notify as runNotify } from "../core/notifier.js";
10
+ import { appendSkillHistory, listSkillHistory, readSkillHistoryRaw, } from "../core/skill-history.js";
10
11
  export async function runTypeScriptSkill(config, manifest, input) {
11
12
  const entry = await resolveSkillEntryPath(manifest);
12
13
  const skillDir = await realpath(manifest.dir);
@@ -113,13 +114,14 @@ function createContext(config, manifest, initialMemory) {
113
114
  { role: "system", content: `You are an AI step "${step.id}" of an Agent-Sin skill. Purpose: ${step.purpose}` },
114
115
  { role: "user", content: typeof payload === "string" ? payload : JSON.stringify(payload) },
115
116
  ];
116
- const response = await getAiProvider()(config, { model_id: modelId, messages });
117
+ const response = await getAiProvider()(config, { model_id: modelId, messages, cwd: config.workspace });
117
118
  return {
118
119
  status: "ok",
119
120
  step_id: stepId,
120
121
  model_id: modelId,
121
122
  provider: response.provider,
122
123
  text: response.text,
124
+ generated_images: response.generated_images || [],
123
125
  };
124
126
  }
125
127
  catch (error) {
@@ -145,6 +147,11 @@ function createContext(config, manifest, initialMemory) {
145
147
  to: args.to,
146
148
  discordThreadId: args.discordThreadId,
147
149
  telegramThreadId: args.telegramThreadId,
150
+ filePath: args.filePath,
151
+ filePaths: args.filePaths,
152
+ imagePath: args.imagePath,
153
+ imagePaths: args.imagePaths,
154
+ cwd: config.workspace,
148
155
  });
149
156
  return { ok: result.ok, channel: result.channel, detail: result.detail };
150
157
  },
@@ -164,6 +171,11 @@ function createContext(config, manifest, initialMemory) {
164
171
  return true;
165
172
  },
166
173
  },
174
+ history: {
175
+ append: async (args) => appendSkillHistory(config, manifest, args),
176
+ list: async (options = {}) => listSkillHistory(config, manifest, options),
177
+ read: async (options = {}) => readSkillHistoryRaw(config, manifest, options),
178
+ },
167
179
  now: () => new Date().toISOString(),
168
180
  },
169
181
  memoryUpdates,
@@ -1,3 +1,14 @@
1
+ export interface SkillInputSources {
2
+ workspace?: string;
3
+ notes_dir?: string;
4
+ memory_dir?: string;
5
+ index_dir?: string;
6
+ logs_dir?: string;
7
+ skill_output_dir?: string;
8
+ skillOutputDir?: string;
9
+ locale?: string;
10
+ [key: string]: unknown;
11
+ }
1
12
  export interface SkillInput {
2
13
  args: Record<string, unknown>;
3
14
  trigger: {
@@ -5,19 +16,23 @@ export interface SkillInput {
5
16
  id: string;
6
17
  time: string;
7
18
  };
8
- sources: Record<string, unknown>;
19
+ sources: SkillInputSources;
9
20
  memory: Record<string, unknown>;
10
21
  }
11
- export type NotifyChannel = "auto" | "macos" | "discord" | "telegram" | "slack" | "mail" | "stderr";
22
+ export type NotifyChannel = "auto" | "macos" | "discord" | "telegram" | "slack" | "mail" | "g2" | "stderr";
12
23
  export interface NotifyArgs {
13
- title: string;
14
- body: string;
24
+ title?: string;
25
+ body?: string;
15
26
  subtitle?: string;
16
27
  sound?: boolean;
17
28
  channel?: NotifyChannel;
18
29
  to?: string;
19
30
  discordThreadId?: string;
20
31
  telegramThreadId?: string;
32
+ filePath?: string;
33
+ filePaths?: string[];
34
+ imagePath?: string;
35
+ imagePaths?: string[];
21
36
  }
22
37
  export interface NotifyOutcome {
23
38
  ok: boolean;
@@ -90,5 +90,6 @@ export declare function shouldUseTelegramDraftStream(message: TelegramMessage):
90
90
  export declare function telegramSendPayload(chatId: string, content: string, options?: TelegramSendOptions): Record<string, unknown>;
91
91
  export declare function telegramDraftPayload(message: TelegramMessage, draftId: number, text: string): Record<string, unknown>;
92
92
  export declare function formatTelegramDraftProgress(event: AiProgressEvent): string | null;
93
+ export declare function formatTelegramChatDraftProgress(event: AiProgressEvent): string | null;
93
94
  export declare function loadTelegramHistories(filePath: string): Promise<Map<string, ChatTurn[]>>;
94
95
  export declare function loadTelegramIntentRuntimes(filePath: string): Promise<Map<string, IntentRuntime>>;
@@ -7,6 +7,7 @@ import { createIntentRuntime, renderBuildFooter, shouldShowBuildFooter, } from "
7
7
  import { routeConversationMessage, } from "../builder/conversation-router.js";
8
8
  import { cleanProgressText, formatBuildProgress, progressIntervalMs } from "../builder/progress-format.js";
9
9
  import { chunkText, cleanAttachmentText, formatAttachmentLabel, formatBytes, guessImageMimeType, indentAttachmentContent, isImageLikeFile, isTextLikeFile, } from "../core/message-utils.js";
10
+ import { collectLocalFileAttachments, collectLocalImageAttachments, } from "../core/image-attachments.js";
10
11
  import { isEmptyIntentRuntime, loadIntentRuntimeMap, saveIntentRuntimeMap, } from "../builder/intent-runtime-store.js";
11
12
  import { inferLocaleFromText, l, lLines, withLocale } from "../core/i18n.js";
12
13
  import { consumeUpdateBanner, scheduleUpdateCheck } from "../core/update-notifier.js";
@@ -351,20 +352,20 @@ async function handleTelegramMessage(state, message) {
351
352
  draft.update(l("Thinking", "考えています"), { force: true });
352
353
  const prevMode = intentRuntime.mode;
353
354
  try {
354
- const lines = await routeTelegramMessage(state, userText, history, intentRuntime, chatKey, chatId, threadId, message.message_id, draft, userMessage.images);
355
+ const routed = await routeTelegramMessage(state, userText, history, intentRuntime, chatKey, chatId, threadId, message.message_id, draft, userMessage.images);
355
356
  typing.stop();
356
357
  trimHistory(history);
357
358
  void saveTelegramHistories(state);
358
359
  void saveTelegramIntentRuntimes(state);
359
360
  const isBuildEntry = prevMode !== "build" && intentRuntime.mode === "build";
360
- const decorated = withTelegramModeBadge(intentRuntime, lines, { userText, isBuildEntry });
361
+ const decorated = withTelegramModeBadge(intentRuntime, routed.lines, { userText, isBuildEntry });
361
362
  scheduleUpdateCheck(state.config.workspace);
362
363
  const banner = await consumeUpdateBanner(state.config.workspace);
363
364
  const finalLines = banner ? [banner, "", ...decorated] : decorated;
364
365
  const reply = finalLines.filter((line) => line !== undefined && line !== null).join("\n").trim();
365
366
  draft.update(l("Sending reply", "応答を送信しています"), { force: true });
366
367
  await draft.finish();
367
- await sendTelegramMessage(state, chatId, reply || l("(no response)", "(応答なし)"), sendOptions);
368
+ await sendTelegramMessageWithLocalAttachments(state, chatId, reply || l("(no response)", "(応答なし)"), sendOptions, routed.localAttachmentPaths);
368
369
  }
369
370
  catch (error) {
370
371
  typing.stop();
@@ -398,7 +399,8 @@ async function refreshTelegramStateConfig(state) {
398
399
  }
399
400
  }
400
401
  async function routeTelegramMessage(state, text, history, intentRuntime, chatKey, chatId, threadId, replyToMessageId, draft, images = []) {
401
- return routeConversationMessage({
402
+ const localAttachmentPaths = [];
403
+ const lines = await routeConversationMessage({
402
404
  config: state.config,
403
405
  text,
404
406
  history,
@@ -408,7 +410,11 @@ async function routeTelegramMessage(state, text, history, intentRuntime, chatKey
408
410
  createBuildProgress: () => createTelegramBuildProgressReporter(state, chatKey, chatId, threadId, replyToMessageId, draft),
409
411
  onChatProgress: (event) => draft.onChatProgress(event),
410
412
  onAiProgress: (event) => draft.onAiProgress(event),
413
+ onLocalAttachments: (paths) => {
414
+ localAttachmentPaths.push(...paths);
415
+ },
411
416
  });
417
+ return { lines, localAttachmentPaths };
412
418
  }
413
419
  function createTelegramBuildProgressReporter(state, chatKey, chatId, threadId, replyToMessageId, draft) {
414
420
  const minIntervalMs = telegramProgressIntervalMs();
@@ -712,7 +718,7 @@ async function persistTelegramAttachmentBuffer(state, attachment, buffer, mimeTy
712
718
  const HH = String(now.getHours()).padStart(2, "0");
713
719
  const mm = String(now.getMinutes()).padStart(2, "0");
714
720
  const ss = String(now.getSeconds()).padStart(2, "0");
715
- const dir = path.join(state.config.notes_dir, "attachments", yyyy, MM);
721
+ const dir = path.join(state.config.memory_dir, "attachments", yyyy, MM);
716
722
  await mkdir(dir, { recursive: true });
717
723
  const ext = pickAttachmentExtension(attachment.filename, mimeType);
718
724
  const random = Math.random().toString(36).slice(2, 8);
@@ -903,6 +909,12 @@ export function formatTelegramDraftProgress(event) {
903
909
  return null;
904
910
  }
905
911
  }
912
+ export function formatTelegramChatDraftProgress(event) {
913
+ if (event.kind === "message") {
914
+ return l("Preparing reply", "応答を整理しています");
915
+ }
916
+ return formatTelegramDraftProgress(event);
917
+ }
906
918
  function createTelegramDraftStreamer(state, message) {
907
919
  if (!shouldUseTelegramDraftStream(message)) {
908
920
  return noopTelegramDraftStreamer();
@@ -969,9 +981,9 @@ function createTelegramDraftStreamer(state, message) {
969
981
  }
970
982
  },
971
983
  onAiProgress(event) {
972
- const text = formatTelegramDraftProgress(event);
984
+ const text = formatTelegramChatDraftProgress(event);
973
985
  if (text)
974
- enqueue(text, event.kind === "message");
986
+ enqueue(text);
975
987
  },
976
988
  onBuildProgress(event) {
977
989
  const text = formatTelegramDraftProgress(event);
@@ -1071,6 +1083,60 @@ async function sendTelegramMessage(state, chatId, content, options = {}) {
1071
1083
  }
1072
1084
  }
1073
1085
  }
1086
+ async function sendTelegramMessageWithLocalAttachments(state, chatId, content, options = {}, localAttachmentPaths = []) {
1087
+ const sanitized = stripInternalControlBlocks(content);
1088
+ const explicitAttachments = await collectLocalFileAttachments({
1089
+ paths: localAttachmentPaths,
1090
+ cwd: state.config.workspace,
1091
+ allowedRoots: [state.config.workspace, state.config.notes_dir],
1092
+ });
1093
+ const inlineImages = await collectLocalImageAttachments({
1094
+ text: sanitized,
1095
+ cwd: state.config.workspace,
1096
+ allowedRoots: [state.config.workspace, state.config.notes_dir],
1097
+ });
1098
+ const attachments = dedupeLocalAttachments([...explicitAttachments, ...inlineImages]);
1099
+ if (sanitized.trim()) {
1100
+ await sendTelegramMessage(state, chatId, sanitized, options);
1101
+ }
1102
+ for (let index = 0; index < attachments.length; index += 1) {
1103
+ await sendTelegramFileAttachment(state, chatId, attachments[index], index === 0 && !sanitized.trim() ? options : { threadId: options.threadId });
1104
+ }
1105
+ }
1106
+ async function sendTelegramFileAttachment(state, chatId, attachment, options = {}) {
1107
+ const method = shouldSendTelegramAsPhoto(attachment) ? "sendPhoto" : "sendDocument";
1108
+ const fileField = method === "sendPhoto" ? "photo" : "document";
1109
+ const payload = telegramFilePayload(chatId, options);
1110
+ try {
1111
+ const buffer = await readFile(attachment.path);
1112
+ await telegramMultipartApi(state, method, payload, fileField, attachment.filename, attachment.mimeType, buffer);
1113
+ }
1114
+ catch (error) {
1115
+ const message = error instanceof Error ? error.message : String(error);
1116
+ console.error(`agent-sin telegram: file send error: ${message}`);
1117
+ await appendEventLog(state.config, {
1118
+ level: "error",
1119
+ source: "telegram",
1120
+ event: "file_send_error",
1121
+ message,
1122
+ details: { chat_id: chatId, path: attachment.path },
1123
+ });
1124
+ }
1125
+ }
1126
+ function shouldSendTelegramAsPhoto(attachment) {
1127
+ return attachment.isImage && ["image/png", "image/jpeg", "image/webp"].includes(attachment.mimeType.toLowerCase());
1128
+ }
1129
+ function dedupeLocalAttachments(attachments) {
1130
+ const seen = new Set();
1131
+ const out = [];
1132
+ for (const attachment of attachments) {
1133
+ if (seen.has(attachment.path))
1134
+ continue;
1135
+ seen.add(attachment.path);
1136
+ out.push(attachment);
1137
+ }
1138
+ return out;
1139
+ }
1074
1140
  async function sendTelegramChatAction(state, chatId, threadId) {
1075
1141
  try {
1076
1142
  const payload = {
@@ -1086,6 +1152,21 @@ async function sendTelegramChatAction(state, chatId, threadId) {
1086
1152
  // typing indicator is a hint; ignore failures
1087
1153
  }
1088
1154
  }
1155
+ function telegramFilePayload(chatId, options = {}) {
1156
+ const payload = {
1157
+ chat_id: chatId,
1158
+ };
1159
+ if (options.threadId) {
1160
+ payload.message_thread_id = options.threadId;
1161
+ }
1162
+ if (options.replyToMessageId && process.env.AGENT_SIN_TELEGRAM_REPLY_TO_MESSAGE !== "0") {
1163
+ payload.reply_parameters = {
1164
+ message_id: options.replyToMessageId,
1165
+ allow_sending_without_reply: true,
1166
+ };
1167
+ }
1168
+ return payload;
1169
+ }
1089
1170
  async function telegramApi(state, method, payload) {
1090
1171
  const response = await fetch(`${TELEGRAM_API_BASE}/bot${state.token}/${method}`, {
1091
1172
  method: "POST",
@@ -1102,6 +1183,33 @@ async function telegramApi(state, method, payload) {
1102
1183
  }
1103
1184
  return data.result;
1104
1185
  }
1186
+ async function telegramMultipartApi(state, method, payload, fileField, filename, mimeType, buffer) {
1187
+ const form = new FormData();
1188
+ for (const [key, value] of Object.entries(payload)) {
1189
+ if (value === undefined || value === null)
1190
+ continue;
1191
+ form.append(key, typeof value === "object" ? JSON.stringify(value) : String(value));
1192
+ }
1193
+ form.append(fileField, blobFromBuffer(buffer, mimeType), filename);
1194
+ const response = await fetch(`${TELEGRAM_API_BASE}/bot${state.token}/${method}`, {
1195
+ method: "POST",
1196
+ body: form,
1197
+ });
1198
+ if (!response.ok) {
1199
+ const detail = await response.text().catch(() => "");
1200
+ throw new Error(`HTTP ${response.status} ${detail.slice(0, 200)}`);
1201
+ }
1202
+ const data = (await response.json());
1203
+ if (!data.ok) {
1204
+ throw new Error(data.description || "Telegram API error");
1205
+ }
1206
+ return data.result;
1207
+ }
1208
+ function blobFromBuffer(buffer, type) {
1209
+ const bytes = new Uint8Array(buffer.length);
1210
+ bytes.set(buffer);
1211
+ return new Blob([bytes.buffer], { type });
1212
+ }
1105
1213
  async function loadTelegramOffset(filePath) {
1106
1214
  try {
1107
1215
  const raw = await readFile(filePath, "utf8");