augure 0.4.2 → 0.5.1

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 (2) hide show
  1. package/dist/bin.js +760 -165
  2. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -50,9 +50,12 @@ var AppConfigSchema = z.object({
50
50
  heartbeatInterval: z.string().min(1),
51
51
  jobs: z.array(z.object({
52
52
  id: z.string().min(1),
53
- cron: z.string().min(1),
53
+ cron: z.string().min(1).optional(),
54
+ runAt: z.string().min(1).optional(),
54
55
  prompt: z.string().min(1),
55
56
  channel: z.string().min(1)
57
+ }).refine((j) => j.cron || j.runAt, {
58
+ message: "Job must have either cron or runAt"
56
59
  }))
57
60
  }),
58
61
  sandbox: z.object({
@@ -150,19 +153,33 @@ async function loadConfig(path) {
150
153
  return AppConfigSchema.parse(parsed);
151
154
  }
152
155
 
156
+ // ../types/dist/logger.js
157
+ var noop = () => {
158
+ };
159
+ var noopLogger = {
160
+ debug: noop,
161
+ info: noop,
162
+ warn: noop,
163
+ error: noop,
164
+ child: () => noopLogger
165
+ };
166
+
153
167
  // ../core/dist/llm.js
154
168
  var OpenRouterClient = class {
155
169
  apiKey;
156
170
  model;
157
171
  maxTokens;
158
172
  baseUrl;
173
+ log;
159
174
  constructor(config) {
160
175
  this.apiKey = config.apiKey;
161
176
  this.model = config.model;
162
177
  this.maxTokens = config.maxTokens;
163
178
  this.baseUrl = config.baseUrl ?? "https://openrouter.ai/api/v1";
179
+ this.log = config.logger ?? noopLogger;
164
180
  }
165
181
  async chat(messages, tools) {
182
+ this.log.debug(`Request: model=${this.model} messages=${messages.length} tools=${tools?.length ?? 0}`);
166
183
  const response = await fetch(`${this.baseUrl}/chat/completions`, {
167
184
  method: "POST",
168
185
  headers: {
@@ -196,6 +213,7 @@ var OpenRouterClient = class {
196
213
  }
197
214
  const data = await response.json();
198
215
  const choice = data.choices[0];
216
+ this.log.debug(`Response: ${response.status} ${data.usage.prompt_tokens}+${data.usage.completion_tokens} tokens`);
199
217
  return {
200
218
  content: choice.message.content ?? "",
201
219
  toolCalls: (choice.message.tool_calls ?? []).map((tc) => {
@@ -203,7 +221,7 @@ var OpenRouterClient = class {
203
221
  try {
204
222
  args = tc.function.arguments ? JSON.parse(tc.function.arguments) : {};
205
223
  } catch {
206
- console.error(`[augure] Failed to parse tool call arguments for ${tc.function.name}:`, tc.function.arguments);
224
+ this.log.warn(`Failed to parse tool call arguments for ${tc.function.name}`);
207
225
  }
208
226
  return { id: tc.id, name: tc.function.name, arguments: args };
209
227
  }),
@@ -219,6 +237,19 @@ var OpenRouterClient = class {
219
237
  function assembleContext(input) {
220
238
  const { systemPrompt, memoryContent, conversationHistory, persona } = input;
221
239
  let system = systemPrompt;
240
+ const now = /* @__PURE__ */ new Date();
241
+ const humanDate = new Intl.DateTimeFormat("en-US", {
242
+ weekday: "long",
243
+ year: "numeric",
244
+ month: "long",
245
+ day: "numeric",
246
+ hour: "2-digit",
247
+ minute: "2-digit",
248
+ timeZoneName: "short"
249
+ }).format(now);
250
+ system += `
251
+
252
+ Current date and time: ${now.toISOString()} (${humanDate})`;
222
253
  if (persona) {
223
254
  system += `
224
255
 
@@ -246,13 +277,15 @@ function summarize(text, maxLen = 200) {
246
277
  }
247
278
  var FileAuditLogger = class {
248
279
  basePath;
280
+ logger;
249
281
  pendingWrite = Promise.resolve();
250
282
  initialized = false;
251
- constructor(basePath) {
283
+ constructor(basePath, logger) {
252
284
  this.basePath = basePath;
285
+ this.logger = logger ?? noopLogger;
253
286
  }
254
287
  log(entry) {
255
- this.pendingWrite = this.pendingWrite.then(() => this.writeEntry(entry)).catch((err2) => console.error("[augure] Audit write error:", err2));
288
+ this.pendingWrite = this.pendingWrite.then(() => this.writeEntry(entry)).catch((err2) => this.logger.error("Audit write error:", err2));
256
289
  }
257
290
  async close() {
258
291
  await this.pendingWrite;
@@ -278,10 +311,12 @@ var NullAuditLogger = class {
278
311
  // ../core/dist/agent.js
279
312
  var Agent = class {
280
313
  config;
314
+ log;
281
315
  conversations = /* @__PURE__ */ new Map();
282
316
  state = "running";
283
317
  constructor(config) {
284
318
  this.config = config;
319
+ this.log = config.logger ?? noopLogger;
285
320
  }
286
321
  getState() {
287
322
  return this.state;
@@ -321,12 +356,14 @@ var Agent = class {
321
356
  conversationHistory: history,
322
357
  persona: this.config.persona
323
358
  });
359
+ this.log.debug(`LLM call #${loopCount + 1} (${messages.length} messages)`);
324
360
  const response = await this.config.llm.chat(messages, toolSchemas);
325
361
  if (response.toolCalls.length === 0) {
326
362
  history.push({
327
363
  role: "assistant",
328
364
  content: response.content
329
365
  });
366
+ this.log.debug(`Response: ${response.usage.inputTokens}+${response.usage.outputTokens} tokens, ${Date.now() - start}ms`);
330
367
  if (this.config.audit) {
331
368
  this.config.audit.log({
332
369
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -344,7 +381,7 @@ var Agent = class {
344
381
  });
345
382
  }
346
383
  if (this.config.ingester) {
347
- this.config.ingester.ingest(history).catch((err2) => console.error("[augure] Ingestion error:", err2));
384
+ this.config.ingester.ingest(history).catch((err2) => this.log.error("Ingestion error:", err2));
348
385
  }
349
386
  return response.content;
350
387
  }
@@ -355,7 +392,9 @@ var Agent = class {
355
392
  });
356
393
  for (const toolCall of response.toolCalls) {
357
394
  const toolStart = Date.now();
395
+ this.log.debug(`Tool: ${toolCall.name}`);
358
396
  const result = await this.config.tools.execute(toolCall.name, toolCall.arguments);
397
+ this.log.debug(`Tool ${toolCall.name}: ${result.success ? "ok" : "fail"} (${Date.now() - toolStart}ms)`);
359
398
  history.push({
360
399
  role: "tool",
361
400
  content: result.output,
@@ -563,6 +602,56 @@ var ContextGuard = class {
563
602
  }
564
603
  };
565
604
 
605
+ // ../core/dist/logger.js
606
+ import { styleText } from "util";
607
+ var LEVELS = {
608
+ debug: 0,
609
+ info: 1,
610
+ warn: 2,
611
+ error: 3,
612
+ silent: 4
613
+ };
614
+ function tag(level) {
615
+ switch (level) {
616
+ case "debug":
617
+ return styleText("magenta", "DBG");
618
+ case "info":
619
+ return styleText("cyan", "INF");
620
+ case "warn":
621
+ return styleText("yellow", "WRN");
622
+ case "error":
623
+ return styleText("red", "ERR");
624
+ }
625
+ }
626
+ function createLogger(opts = {}) {
627
+ const min = LEVELS[opts.level ?? "info"];
628
+ const scope = opts.scope;
629
+ function emit(level, msg, args) {
630
+ if (LEVELS[level] < min)
631
+ return;
632
+ const ts = styleText("dim", (/* @__PURE__ */ new Date()).toISOString().slice(11, 23));
633
+ const lvl = tag(level);
634
+ const sc = scope ? ` ${styleText("dim", scope)}` : "";
635
+ const prefix2 = `${styleText("yellow", "\u25B2")} ${ts} ${lvl}${sc}`;
636
+ const fn = level === "error" ? console.error : level === "warn" ? console.warn : console.log;
637
+ if (args.length > 0) {
638
+ fn(prefix2, msg, ...args);
639
+ } else {
640
+ fn(prefix2, msg);
641
+ }
642
+ }
643
+ return {
644
+ debug: (msg, ...args) => emit("debug", msg, args),
645
+ info: (msg, ...args) => emit("info", msg, args),
646
+ warn: (msg, ...args) => emit("warn", msg, args),
647
+ error: (msg, ...args) => emit("error", msg, args),
648
+ child: (childScope) => createLogger({
649
+ level: opts.level,
650
+ scope: scope ? `${scope}:${childScope}` : childScope
651
+ })
652
+ };
653
+ }
654
+
566
655
  // ../channels/dist/telegram/telegram.js
567
656
  import { Bot } from "grammy";
568
657
 
@@ -582,62 +671,6 @@ function createOutgoingPipeline(middlewares, send) {
582
671
  };
583
672
  }
584
673
 
585
- // ../channels/dist/middleware/escape-markdown.js
586
- var SPECIAL_CHARS = /* @__PURE__ */ new Set([".", "!", ">", "#", "+", "-", "=", "|", "{", "}", "~"]);
587
- function escapeMarkdownV2(text) {
588
- if (!text)
589
- return "";
590
- const parts = [];
591
- let i = 0;
592
- while (i < text.length) {
593
- if (text.startsWith("```", i)) {
594
- const endIdx = text.indexOf("```", i + 3);
595
- if (endIdx !== -1) {
596
- parts.push(text.slice(i, endIdx + 3));
597
- i = endIdx + 3;
598
- continue;
599
- }
600
- }
601
- if (text[i] === "`") {
602
- const endIdx = text.indexOf("`", i + 1);
603
- if (endIdx !== -1) {
604
- parts.push(text.slice(i, endIdx + 1));
605
- i = endIdx + 1;
606
- continue;
607
- }
608
- }
609
- if (text[i] === "*" || text[i] === "_") {
610
- parts.push(text[i]);
611
- i++;
612
- continue;
613
- }
614
- if (text[i] === "[" || text[i] === "(") {
615
- parts.push(text[i]);
616
- i++;
617
- continue;
618
- }
619
- if (text[i] === "]") {
620
- parts.push(text[i]);
621
- i++;
622
- continue;
623
- }
624
- const char = text[i];
625
- if (SPECIAL_CHARS.has(char)) {
626
- parts.push(`\\${char}`);
627
- } else {
628
- parts.push(char);
629
- }
630
- i++;
631
- }
632
- return parts.join("");
633
- }
634
- function createEscapeMarkdownMiddleware() {
635
- return async (message, next) => {
636
- message.text = escapeMarkdownV2(message.text);
637
- await next();
638
- };
639
- }
640
-
641
674
  // ../channels/dist/middleware/split-message.js
642
675
  var TELEGRAM_MAX = 4096;
643
676
  function splitText(text, maxLength) {
@@ -743,6 +776,38 @@ async function withRetry(fn, options) {
743
776
  throw lastError;
744
777
  }
745
778
 
779
+ // ../channels/dist/middleware/markdown-to-html.js
780
+ function markdownToTelegramHtml(text) {
781
+ if (!text)
782
+ return "";
783
+ const placeholders = [];
784
+ const PH_PREFIX = "\uFFFCPH";
785
+ const PH_SUFFIX = "\uFFFC";
786
+ function hold(html) {
787
+ const idx = placeholders.length;
788
+ placeholders.push(html);
789
+ return `${PH_PREFIX}${idx}${PH_SUFFIX}`;
790
+ }
791
+ let out = text;
792
+ out = out.replace(/```(\w*)\n([\s\S]*?)```/g, (_, lang, code) => hold(lang ? `<pre><code class="language-${escapeHtml(lang)}">${escapeHtml(code.trimEnd())}</code></pre>` : `<pre>${escapeHtml(code.trimEnd())}</pre>`));
793
+ out = out.replace(/`([^`\n]+)`/g, (_, code) => hold(`<code>${escapeHtml(code)}</code>`));
794
+ out = out.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, linkText, url) => hold(`<a href="${escapeHtml(url)}">${escapeHtml(linkText)}</a>`));
795
+ out = escapeHtml(out);
796
+ out = out.replace(/\*\*\*(.+?)\*\*\*/g, "<b><i>$1</i></b>");
797
+ out = out.replace(/\*\*(.+?)\*\*/g, "<b>$1</b>");
798
+ out = out.replace(/\*([^*\n]+?)\*/g, "<i>$1</i>");
799
+ out = out.replace(/~~(.+?)~~/g, "<s>$1</s>");
800
+ out = out.replace(/^#{1,6}\s+(.+)$/gm, "<b>$1</b>");
801
+ out = out.replace(/^&gt;\s?(.*)$/gm, "<blockquote>$1</blockquote>");
802
+ out = out.replace(/<\/blockquote>\n<blockquote>/g, "\n");
803
+ const phPattern = new RegExp(`${PH_PREFIX}(\\d+)${PH_SUFFIX}`, "g");
804
+ out = out.replace(phPattern, (_, idx) => placeholders[Number(idx)]);
805
+ return out;
806
+ }
807
+ function escapeHtml(text) {
808
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
809
+ }
810
+
746
811
  // ../channels/dist/telegram/media.js
747
812
  function registerMediaHandlers(bot, isAllowed, handlers, onRejected) {
748
813
  bot.on("message:photo", async (ctx) => {
@@ -807,11 +872,13 @@ var TelegramChannel = class {
807
872
  allowedUsers;
808
873
  handlers = [];
809
874
  sendPipeline;
875
+ log;
810
876
  constructor(config) {
877
+ this.log = config.logger ?? noopLogger;
811
878
  this.bot = new Bot(config.botToken);
812
879
  this.allowedUsers = new Set(config.allowedUsers);
813
880
  this.bot.catch((err2) => {
814
- console.error("[augure:telegram] Bot error:", err2.message ?? err2);
881
+ this.log.error("Bot error:", err2.message ?? err2);
815
882
  });
816
883
  this.bot.on("message:text", async (ctx) => {
817
884
  const userId = ctx.from.id;
@@ -832,29 +899,26 @@ var TelegramChannel = class {
832
899
  }
833
900
  });
834
901
  registerMediaHandlers(this.bot, (id) => this.isUserAllowed(id), this.handlers, (userId, ts) => this.handleRejected(userId, Math.floor(ts.getTime() / 1e3), config.rejectMessage));
835
- const rawSend = async (msg) => {
836
- await withRetry(() => this.bot.api.sendMessage(Number(msg.userId), msg.text, {
837
- parse_mode: "MarkdownV2",
838
- ...msg.replyTo ? { reply_parameters: { message_id: Number(msg.replyTo) } } : {}
902
+ const convertAndSend = async (msg) => {
903
+ const htmlText = markdownToTelegramHtml(msg.text);
904
+ const replyOpts = msg.replyTo ? { reply_parameters: { message_id: Number(msg.replyTo) } } : {};
905
+ await withRetry(() => this.bot.api.sendMessage(Number(msg.userId), htmlText, {
906
+ parse_mode: "HTML",
907
+ ...replyOpts
839
908
  }), { maxRetries: 3, baseDelayMs: 500 }).catch(async () => {
840
- await this.bot.api.sendMessage(Number(msg.userId), msg.text, {
841
- ...msg.replyTo ? { reply_parameters: { message_id: Number(msg.replyTo) } } : {}
842
- }).catch((fallbackErr) => {
843
- console.error("[augure:telegram] Fallback send also failed:", fallbackErr);
909
+ await this.bot.api.sendMessage(Number(msg.userId), msg.text, replyOpts).catch((fallbackErr) => {
910
+ this.log.error("Fallback send also failed:", fallbackErr);
844
911
  throw fallbackErr;
845
912
  });
846
913
  });
847
914
  };
848
- this.sendPipeline = createOutgoingPipeline([
849
- createEscapeMarkdownMiddleware(),
850
- createSplitMessageMiddleware(rawSend)
851
- ], rawSend);
915
+ this.sendPipeline = createOutgoingPipeline([createSplitMessageMiddleware(convertAndSend)], convertAndSend);
852
916
  }
853
917
  isUserAllowed(userId) {
854
918
  return this.allowedUsers.has(userId);
855
919
  }
856
920
  handleRejected(userId, unixTimestamp, rejectMessage) {
857
- console.warn(`[augure:telegram] Rejected message from unauthorized user ${userId} at ${new Date(unixTimestamp * 1e3).toISOString()}`);
921
+ this.log.warn(`Rejected message from unauthorized user ${userId} at ${new Date(unixTimestamp * 1e3).toISOString()}`);
858
922
  if (rejectMessage) {
859
923
  this.bot.api.sendMessage(userId, rejectMessage).catch(() => {
860
924
  });
@@ -888,14 +952,27 @@ var ToolRegistry = class {
888
952
  return Array.from(this.tools.values());
889
953
  }
890
954
  toFunctionSchemas() {
891
- return this.list().map((tool) => ({
892
- type: "function",
893
- function: {
894
- name: tool.name,
895
- description: tool.description,
896
- parameters: tool.parameters
955
+ return this.list().map((tool) => {
956
+ let description = tool.description;
957
+ if (tool.configCheck && this.context) {
958
+ try {
959
+ const warning = tool.configCheck(this.context);
960
+ if (warning) {
961
+ description += `
962
+ [NOT CONFIGURED] ${warning}`;
963
+ }
964
+ } catch {
965
+ }
897
966
  }
898
- }));
967
+ return {
968
+ type: "function",
969
+ function: {
970
+ name: tool.name,
971
+ description,
972
+ parameters: tool.parameters
973
+ }
974
+ };
975
+ });
899
976
  }
900
977
  async execute(name, params) {
901
978
  const tool = this.tools.get(name);
@@ -967,7 +1044,7 @@ var memoryWriteTool = {
967
1044
  // ../tools/dist/schedule.js
968
1045
  var scheduleTool = {
969
1046
  name: "schedule",
970
- description: "Manage scheduled tasks: create, delete, or list cron jobs",
1047
+ description: "Manage scheduled tasks: create recurring (cron) or one-shot (runAt) jobs, delete, or list them",
971
1048
  parameters: {
972
1049
  type: "object",
973
1050
  properties: {
@@ -977,13 +1054,14 @@ var scheduleTool = {
977
1054
  description: "The scheduling action to perform"
978
1055
  },
979
1056
  id: { type: "string", description: "The schedule ID (for create/delete)" },
980
- cron: { type: "string", description: "Cron expression (for create)" },
1057
+ cron: { type: "string", description: "Cron expression for recurring jobs (for create)" },
1058
+ runAt: { type: "string", description: "ISO 8601 date for one-shot jobs, e.g. 2025-03-15T14:00:00Z (for create)" },
981
1059
  prompt: { type: "string", description: "The prompt to execute on schedule (for create)" }
982
1060
  },
983
1061
  required: ["action"]
984
1062
  },
985
1063
  execute: async (params, ctx) => {
986
- const { action, id, cron, prompt } = params;
1064
+ const { action, id, cron, runAt, prompt } = params;
987
1065
  try {
988
1066
  switch (action) {
989
1067
  case "list": {
@@ -991,22 +1069,29 @@ var scheduleTool = {
991
1069
  if (jobs.length === 0) {
992
1070
  return { success: true, output: "No scheduled jobs." };
993
1071
  }
994
- const lines = jobs.map((j) => `- ${j.id}: "${j.prompt}" @ ${j.cron} (${j.enabled ? "enabled" : "disabled"})`);
1072
+ const lines = jobs.map((j) => {
1073
+ const schedule = j.cron ? `cron: ${j.cron}` : `runAt: ${j.runAt}`;
1074
+ return `- ${j.id}: "${j.prompt}" @ ${schedule} (${j.enabled ? "enabled" : "disabled"})`;
1075
+ });
995
1076
  return { success: true, output: lines.join("\n") };
996
1077
  }
997
1078
  case "create": {
998
- if (!cron || !prompt) {
999
- return { success: false, output: "Missing required fields: cron and prompt" };
1079
+ if (!prompt) {
1080
+ return { success: false, output: "Missing required field: prompt" };
1081
+ }
1082
+ if (!cron && !runAt) {
1083
+ return { success: false, output: "Must provide either cron (recurring) or runAt (one-shot)" };
1000
1084
  }
1001
1085
  const jobId = id ?? `job-${Date.now()}`;
1002
1086
  ctx.scheduler.addJob({
1003
1087
  id: jobId,
1004
1088
  cron,
1089
+ runAt,
1005
1090
  prompt,
1006
1091
  channel: "default",
1007
1092
  enabled: true
1008
1093
  });
1009
- return { success: true, output: `Created job ${jobId}` };
1094
+ return { success: true, output: `Created job ${jobId} (${cron ? "recurring" : `one-shot at ${runAt}`})` };
1010
1095
  }
1011
1096
  case "delete": {
1012
1097
  if (!id) {
@@ -1027,10 +1112,54 @@ var scheduleTool = {
1027
1112
  }
1028
1113
  };
1029
1114
 
1115
+ // ../tools/dist/datetime.js
1116
+ var datetimeTool = {
1117
+ name: "datetime",
1118
+ description: "Get the current date and time, optionally in a specific timezone",
1119
+ parameters: {
1120
+ type: "object",
1121
+ properties: {
1122
+ timezone: {
1123
+ type: "string",
1124
+ description: "IANA timezone (e.g. 'Europe/Paris', 'America/New_York'). Defaults to the system timezone."
1125
+ }
1126
+ }
1127
+ },
1128
+ execute: async (params) => {
1129
+ const { timezone } = params;
1130
+ const now = /* @__PURE__ */ new Date();
1131
+ const options = {
1132
+ weekday: "long",
1133
+ year: "numeric",
1134
+ month: "long",
1135
+ day: "numeric",
1136
+ hour: "2-digit",
1137
+ minute: "2-digit",
1138
+ second: "2-digit",
1139
+ timeZoneName: "longOffset"
1140
+ };
1141
+ if (timezone) {
1142
+ options.timeZone = timezone;
1143
+ }
1144
+ try {
1145
+ const formatted = new Intl.DateTimeFormat("en-US", options).format(now);
1146
+ return {
1147
+ success: true,
1148
+ output: `${formatted}
1149
+ ISO 8601 (UTC): ${now.toISOString()}
1150
+ Unix timestamp: ${Math.floor(now.getTime() / 1e3)}`
1151
+ };
1152
+ } catch {
1153
+ return { success: false, output: `Invalid timezone: ${timezone}` };
1154
+ }
1155
+ }
1156
+ };
1157
+
1030
1158
  // ../tools/dist/web-search.js
1031
1159
  var webSearchTool = {
1032
1160
  name: "web_search",
1033
1161
  description: "Search the web using the configured search provider (Tavily, Exa, or SearXNG)",
1162
+ configCheck: (ctx) => ctx.config.tools?.webSearch ? null : "This tool requires configuration. See https://augure.dev/docs/tools/web-search",
1034
1163
  parameters: {
1035
1164
  type: "object",
1036
1165
  properties: {
@@ -1242,6 +1371,273 @@ ${text}`;
1242
1371
  // ../tools/dist/email.js
1243
1372
  import { ImapFlow } from "imapflow";
1244
1373
  import { createTransport } from "nodemailer";
1374
+ var MAX_BODY_CHARS = 4e3;
1375
+ function stripHtml(html) {
1376
+ return html.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "").replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n\n").replace(/<\/div>/gi, "\n").replace(/<[^>]+>/g, "").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/\n{3,}/g, "\n\n").trim();
1377
+ }
1378
+ function findTextPart(structure) {
1379
+ if (structure.type === "text/plain" && structure.part) {
1380
+ return { part: structure.part, isHtml: false };
1381
+ }
1382
+ if (structure.type === "text/html" && structure.part) {
1383
+ return { part: structure.part, isHtml: true };
1384
+ }
1385
+ if (structure.childNodes) {
1386
+ let htmlFallback = null;
1387
+ for (const child of structure.childNodes) {
1388
+ const found = findTextPart(child);
1389
+ if (found && !found.isHtml)
1390
+ return found;
1391
+ if (found && found.isHtml)
1392
+ htmlFallback = found;
1393
+ }
1394
+ return htmlFallback;
1395
+ }
1396
+ return null;
1397
+ }
1398
+ async function withImapClient(config, fn) {
1399
+ const client = new ImapFlow({
1400
+ host: config.host,
1401
+ port: config.port,
1402
+ secure: config.port === 993,
1403
+ auth: { user: config.user, pass: config.password },
1404
+ logger: false
1405
+ });
1406
+ let connected = false;
1407
+ try {
1408
+ await client.connect();
1409
+ connected = true;
1410
+ return await fn(client);
1411
+ } finally {
1412
+ if (connected) {
1413
+ await client.logout();
1414
+ }
1415
+ }
1416
+ }
1417
+ function formatSummaries(emails) {
1418
+ if (emails.length === 0)
1419
+ return "No emails found.";
1420
+ return emails.map((e, i) => `${i + 1}. ${e.seen ? "" : "[UNREAD] "}UID:${e.uid} Subject: ${e.subject} | From: ${e.from} | Date: ${e.date}`).join("\n");
1421
+ }
1422
+ function extractAddress(addr) {
1423
+ if (!addr)
1424
+ return "unknown";
1425
+ if (Array.isArray(addr)) {
1426
+ const first = addr[0];
1427
+ if (first && typeof first === "object" && "address" in first) {
1428
+ return first.address;
1429
+ }
1430
+ return String(first ?? "unknown");
1431
+ }
1432
+ if (typeof addr === "object" && "address" in addr) {
1433
+ return addr.address;
1434
+ }
1435
+ return String(addr);
1436
+ }
1437
+ async function handleList(params, imapConfig) {
1438
+ const folder = params.folder ?? "INBOX";
1439
+ const limit = params.limit ?? 10;
1440
+ return withImapClient(imapConfig, async (client) => {
1441
+ const lock = await client.getMailboxLock(folder);
1442
+ try {
1443
+ const status = client.mailbox;
1444
+ if (!status || status.exists === 0)
1445
+ return "Mailbox is empty.";
1446
+ const start = Math.max(1, status.exists - limit + 1);
1447
+ const emails = [];
1448
+ for await (const msg of client.fetch(`${start}:*`, {
1449
+ envelope: true,
1450
+ flags: true
1451
+ })) {
1452
+ emails.push({
1453
+ uid: msg.uid,
1454
+ subject: msg.envelope?.subject ?? "(no subject)",
1455
+ from: extractAddress(msg.envelope?.from),
1456
+ date: msg.envelope?.date?.toISOString() ?? "unknown",
1457
+ seen: msg.flags?.has("\\Seen") ?? false
1458
+ });
1459
+ }
1460
+ return formatSummaries(emails.slice(-limit));
1461
+ } finally {
1462
+ lock.release();
1463
+ }
1464
+ });
1465
+ }
1466
+ async function handleRead(params, imapConfig) {
1467
+ const folder = params.folder ?? "INBOX";
1468
+ return withImapClient(imapConfig, async (client) => {
1469
+ const lock = await client.getMailboxLock(folder);
1470
+ try {
1471
+ const msg = await client.fetchOne(String(params.uid), {
1472
+ envelope: true,
1473
+ bodyStructure: true,
1474
+ flags: true,
1475
+ uid: true
1476
+ });
1477
+ if (!msg)
1478
+ return `No email found with UID ${params.uid}.`;
1479
+ let bodyText = "";
1480
+ const textPart = msg.bodyStructure ? findTextPart(msg.bodyStructure) : null;
1481
+ if (textPart) {
1482
+ const { content } = await client.download(String(params.uid), textPart.part, {
1483
+ uid: true
1484
+ });
1485
+ const chunks = [];
1486
+ for await (const chunk of content) {
1487
+ chunks.push(Buffer.from(chunk));
1488
+ }
1489
+ bodyText = Buffer.concat(chunks).toString("utf-8");
1490
+ if (textPart.isHtml)
1491
+ bodyText = stripHtml(bodyText);
1492
+ }
1493
+ if (bodyText.length > MAX_BODY_CHARS) {
1494
+ bodyText = bodyText.slice(0, MAX_BODY_CHARS) + "\n[truncated]";
1495
+ }
1496
+ const wasSeen = msg.flags?.has("\\Seen") ?? false;
1497
+ await client.messageFlagsAdd(String(params.uid), ["\\Seen"], { uid: true });
1498
+ const subject = msg.envelope?.subject ?? "(no subject)";
1499
+ const from = extractAddress(msg.envelope?.from);
1500
+ const date = msg.envelope?.date?.toISOString() ?? "unknown";
1501
+ return `UID: ${params.uid}
1502
+ Subject: ${subject}
1503
+ From: ${from}
1504
+ Date: ${date}
1505
+ Status: ${wasSeen ? "was read" : "was unread, marked read"}
1506
+
1507
+ ${bodyText}`;
1508
+ } finally {
1509
+ lock.release();
1510
+ }
1511
+ });
1512
+ }
1513
+ async function handleSearch(params, imapConfig) {
1514
+ const folder = params.folder ?? "INBOX";
1515
+ const limit = params.limit ?? 10;
1516
+ return withImapClient(imapConfig, async (client) => {
1517
+ const lock = await client.getMailboxLock(folder);
1518
+ try {
1519
+ const criteria = {};
1520
+ if (params.from)
1521
+ criteria.from = params.from;
1522
+ if (params.subject)
1523
+ criteria.subject = params.subject;
1524
+ if (params.since)
1525
+ criteria.since = new Date(params.since);
1526
+ if (params.unseen)
1527
+ criteria.seen = false;
1528
+ const result = await client.search(criteria, { uid: true });
1529
+ const uids = Array.isArray(result) ? result : [];
1530
+ if (uids.length === 0)
1531
+ return "No emails match the search criteria.";
1532
+ const selected = uids.slice(-limit);
1533
+ const emails = [];
1534
+ for await (const msg of client.fetch(selected, { envelope: true, flags: true }, { uid: true })) {
1535
+ emails.push({
1536
+ uid: msg.uid,
1537
+ subject: msg.envelope?.subject ?? "(no subject)",
1538
+ from: extractAddress(msg.envelope?.from),
1539
+ date: msg.envelope?.date?.toISOString() ?? "unknown",
1540
+ seen: msg.flags?.has("\\Seen") ?? false
1541
+ });
1542
+ }
1543
+ return formatSummaries(emails);
1544
+ } finally {
1545
+ lock.release();
1546
+ }
1547
+ });
1548
+ }
1549
+ async function handleSend(params, smtpConfig) {
1550
+ const transport = createTransport({
1551
+ host: smtpConfig.host,
1552
+ port: smtpConfig.port,
1553
+ secure: smtpConfig.port === 465,
1554
+ auth: { user: smtpConfig.user, pass: smtpConfig.password }
1555
+ });
1556
+ const info = await transport.sendMail({
1557
+ from: smtpConfig.user,
1558
+ to: params.to,
1559
+ subject: params.subject,
1560
+ text: params.body,
1561
+ cc: params.cc,
1562
+ bcc: params.bcc
1563
+ });
1564
+ return `Email sent. Message ID: ${info.messageId}`;
1565
+ }
1566
+ var emailTool = {
1567
+ name: "email",
1568
+ description: "Manage email: list recent messages, read by UID, search with criteria, or send an email via SMTP",
1569
+ configCheck: (ctx) => ctx.config.tools?.email ? null : "This tool requires configuration. See https://augure.dev/docs/tools/email",
1570
+ parameters: {
1571
+ type: "object",
1572
+ properties: {
1573
+ action: {
1574
+ type: "string",
1575
+ enum: ["list", "read", "search", "send"],
1576
+ description: "The email action to perform"
1577
+ },
1578
+ folder: {
1579
+ type: "string",
1580
+ description: 'IMAP folder (default: "INBOX"). Used by list, read, search.'
1581
+ },
1582
+ limit: {
1583
+ type: "number",
1584
+ description: "Max emails to return (default: 10). Used by list, search."
1585
+ },
1586
+ uid: { type: "number", description: "Email UID to read. Required for read." },
1587
+ from: { type: "string", description: "Filter by sender address. Used by search." },
1588
+ subject: {
1589
+ type: "string",
1590
+ description: "Filter by subject (search) or email subject (send)."
1591
+ },
1592
+ since: {
1593
+ type: "string",
1594
+ description: "ISO 8601 date \u2014 emails since this date. Used by search."
1595
+ },
1596
+ unseen: { type: "boolean", description: "Only unread emails. Used by search." },
1597
+ to: { type: "string", description: "Recipient address. Required for send." },
1598
+ body: { type: "string", description: "Email body text. Required for send." },
1599
+ cc: { type: "string", description: "CC recipients. Used by send." },
1600
+ bcc: { type: "string", description: "BCC recipients. Used by send." }
1601
+ },
1602
+ required: ["action"]
1603
+ },
1604
+ execute: async (params, ctx) => {
1605
+ const p = params;
1606
+ const emailConfig = ctx.config.tools?.email;
1607
+ if (!emailConfig) {
1608
+ return { success: false, output: "Email is not configured. Add tools.email to your config." };
1609
+ }
1610
+ try {
1611
+ switch (p.action) {
1612
+ case "list":
1613
+ return { success: true, output: await handleList(p, emailConfig.imap) };
1614
+ case "read":
1615
+ if (!p.uid)
1616
+ return { success: false, output: "Missing required field: uid" };
1617
+ return { success: true, output: await handleRead(p, emailConfig.imap) };
1618
+ case "search":
1619
+ if (!p.from && !p.subject && !p.since && p.unseen === void 0)
1620
+ return { success: false, output: "At least one search criterion is required (from, subject, since, or unseen)." };
1621
+ return { success: true, output: await handleSearch(p, emailConfig.imap) };
1622
+ case "send":
1623
+ if (!p.to)
1624
+ return { success: false, output: "Missing required field: to" };
1625
+ if (!p.subject)
1626
+ return { success: false, output: "Missing required field: subject" };
1627
+ if (!p.body)
1628
+ return { success: false, output: "Missing required field: body" };
1629
+ return { success: true, output: await handleSend(p, emailConfig.smtp) };
1630
+ default:
1631
+ return { success: false, output: `Unknown action: ${p.action}` };
1632
+ }
1633
+ } catch (err2) {
1634
+ return {
1635
+ success: false,
1636
+ output: err2 instanceof Error ? err2.message : String(err2)
1637
+ };
1638
+ }
1639
+ }
1640
+ };
1245
1641
 
1246
1642
  // ../tools/dist/sandbox-exec.js
1247
1643
  var sandboxExecTool = {
@@ -1320,6 +1716,7 @@ function shellEscape(s) {
1320
1716
  var opencodeTool = {
1321
1717
  name: "opencode",
1322
1718
  description: "Run a code agent (claude-code, opencode, codex CLI) in a Docker container to perform a coding task.",
1719
+ configCheck: (ctx) => ctx.config.sandbox.codeAgent ? null : "This tool requires sandbox.codeAgent configuration. See https://augure.dev/docs/sandbox",
1323
1720
  parameters: {
1324
1721
  type: "object",
1325
1722
  properties: {
@@ -1531,6 +1928,10 @@ var DockerContainer = class {
1531
1928
  }
1532
1929
  });
1533
1930
  this.demux(stream, stdoutPT, stderrPT);
1931
+ stream.on("end", () => {
1932
+ stdoutPT.end();
1933
+ stderrPT.end();
1934
+ });
1534
1935
  });
1535
1936
  }
1536
1937
  };
@@ -1556,6 +1957,7 @@ var DockerContainerPool = class {
1556
1957
  docker;
1557
1958
  image;
1558
1959
  maxTotal;
1960
+ log;
1559
1961
  // C3: idle cache keyed by trust level to prevent cross-trust reuse
1560
1962
  idle = /* @__PURE__ */ new Map([
1561
1963
  ["sandboxed", /* @__PURE__ */ new Set()],
@@ -1567,6 +1969,7 @@ var DockerContainerPool = class {
1567
1969
  this.docker = docker;
1568
1970
  this.image = config.image;
1569
1971
  this.maxTotal = config.maxTotal;
1972
+ this.log = config.logger ?? noopLogger;
1570
1973
  }
1571
1974
  get idleCount() {
1572
1975
  let count = 0;
@@ -1576,18 +1979,22 @@ var DockerContainerPool = class {
1576
1979
  }
1577
1980
  /* ---- acquire ---- */
1578
1981
  async acquire(opts) {
1982
+ this.log.debug(`Acquiring container: trust=${opts.trust} memory=${opts.memory} cpu=${opts.cpu}`);
1579
1983
  const trustIdle = this.idle.get(opts.trust);
1580
1984
  const cached = trustIdle.values().next();
1581
1985
  if (!cached.done) {
1582
1986
  const container2 = cached.value;
1583
1987
  trustIdle.delete(container2);
1584
1988
  this.busy.add(container2);
1989
+ this.log.debug(`Reusing cached container: ${container2.id.slice(0, 12)}`);
1585
1990
  return container2;
1586
1991
  }
1587
1992
  const total = this.idleCount + this.busy.size;
1588
1993
  if (total >= this.maxTotal) {
1994
+ this.log.error(`Pool limit reached: ${total}/${this.maxTotal}`);
1589
1995
  throw new Error("Pool limit reached");
1590
1996
  }
1997
+ this.log.debug("Creating new container...");
1591
1998
  const raw = await this.docker.createContainer(this.buildCreateOpts(opts));
1592
1999
  await raw.start();
1593
2000
  const modem = this.docker.modem;
@@ -1595,6 +2002,7 @@ var DockerContainerPool = class {
1595
2002
  const container = new DockerContainer(raw, demux);
1596
2003
  this.containerTrust.set(container.id, opts.trust);
1597
2004
  this.busy.add(container);
2005
+ this.log.debug(`Container created: ${container.id.slice(0, 12)}`);
1598
2006
  return container;
1599
2007
  }
1600
2008
  /* ---- release ---- */
@@ -1606,6 +2014,7 @@ var DockerContainerPool = class {
1606
2014
  }
1607
2015
  const trust = this.containerTrust.get(container.id) ?? "sandboxed";
1608
2016
  this.idle.get(trust).add(container);
2017
+ this.log.debug(`Container released: ${container.id.slice(0, 12)} \u2192 idle (${trust})`);
1609
2018
  }
1610
2019
  /* ---- destroy ---- */
1611
2020
  async destroy(container) {
@@ -1665,6 +2074,69 @@ var DockerContainerPool = class {
1665
2074
  }
1666
2075
  };
1667
2076
 
2077
+ // ../sandbox/dist/ensure-image.js
2078
+ import { Readable } from "stream";
2079
+ var DOCKERFILE = `FROM node:22-slim
2080
+ RUN apt-get update && apt-get install -y --no-install-recommends \\
2081
+ python3 curl jq git \\
2082
+ && rm -rf /var/lib/apt/lists/*
2083
+ WORKDIR /workspace
2084
+ `;
2085
+ function buildTar(content) {
2086
+ const data = Buffer.from(content, "utf-8");
2087
+ const name = "Dockerfile";
2088
+ const header = Buffer.alloc(512, 0);
2089
+ header.write(name, 0, 100, "utf-8");
2090
+ header.write("0000644\0", 100, 8, "utf-8");
2091
+ header.write("0000000\0", 108, 8, "utf-8");
2092
+ header.write("0000000\0", 116, 8, "utf-8");
2093
+ header.write(data.length.toString(8).padStart(11, "0") + "\0", 124, 12, "utf-8");
2094
+ header.write(Math.floor(Date.now() / 1e3).toString(8).padStart(11, "0") + "\0", 136, 12, "utf-8");
2095
+ header.write("0", 156, 1, "utf-8");
2096
+ header.fill(32, 148, 156);
2097
+ let checksum = 0;
2098
+ for (let i = 0; i < 512; i++)
2099
+ checksum += header[i];
2100
+ header.write(checksum.toString(8).padStart(6, "0") + "\0 ", 148, 8, "utf-8");
2101
+ const padding = 512 - (data.length % 512 || 512);
2102
+ const dataPadded = padding > 0 && padding < 512 ? Buffer.concat([data, Buffer.alloc(padding, 0)]) : data;
2103
+ const end = Buffer.alloc(1024, 0);
2104
+ return Buffer.concat([header, dataPadded, end]);
2105
+ }
2106
+ async function ensureImage(docker, imageName, logger) {
2107
+ const log = logger ?? noopLogger;
2108
+ log.debug(`Checking image: ${imageName}`);
2109
+ try {
2110
+ await docker.getImage(imageName).inspect();
2111
+ log.debug("Image exists");
2112
+ return;
2113
+ } catch (err2) {
2114
+ const statusCode = err2.statusCode;
2115
+ if (statusCode !== void 0 && statusCode !== 404)
2116
+ throw err2;
2117
+ }
2118
+ log.info(`Image "${imageName}" not found, building...`);
2119
+ const tar = buildTar(DOCKERFILE);
2120
+ const stream = await docker.buildImage(Readable.from(tar), {
2121
+ t: imageName
2122
+ });
2123
+ await new Promise((resolve5, reject) => {
2124
+ docker.modem.followProgress(stream, (err2) => {
2125
+ if (err2)
2126
+ reject(err2);
2127
+ else
2128
+ resolve5();
2129
+ }, (event) => {
2130
+ if (event.stream) {
2131
+ const line = event.stream.trim();
2132
+ if (line)
2133
+ log.debug(line);
2134
+ }
2135
+ });
2136
+ });
2137
+ log.info(`Image "${imageName}" built`);
2138
+ }
2139
+
1668
2140
  // ../memory/dist/store.js
1669
2141
  import { readFile as readFile3, writeFile, mkdir as mkdir2, readdir as readdir2, access } from "fs/promises";
1670
2142
  import { join as join3, dirname, relative } from "path";
@@ -1828,6 +2300,7 @@ var CronScheduler = class {
1828
2300
  store;
1829
2301
  jobs = /* @__PURE__ */ new Map();
1830
2302
  tasks = /* @__PURE__ */ new Map();
2303
+ timers = /* @__PURE__ */ new Map();
1831
2304
  handlers = [];
1832
2305
  persistChain = Promise.resolve();
1833
2306
  constructor(store) {
@@ -1837,11 +2310,17 @@ var CronScheduler = class {
1837
2310
  this.handlers.push(handler);
1838
2311
  }
1839
2312
  addJob(job) {
1840
- if (!validate(job.cron)) {
2313
+ if (!job.cron && !job.runAt) {
2314
+ throw new Error(`Job ${job.id} must have either cron or runAt`);
2315
+ }
2316
+ if (job.cron && !validate(job.cron)) {
1841
2317
  throw new Error(`Invalid cron expression: ${job.cron}`);
1842
2318
  }
2319
+ if (job.runAt && isNaN(Date.parse(job.runAt))) {
2320
+ throw new Error(`Invalid runAt date: ${job.runAt}`);
2321
+ }
1843
2322
  this.jobs.set(job.id, job);
1844
- if (job.enabled) {
2323
+ if (job.enabled && job.cron) {
1845
2324
  const task = createTask(job.cron, () => {
1846
2325
  void this.executeHandlers(job);
1847
2326
  });
@@ -1855,6 +2334,11 @@ var CronScheduler = class {
1855
2334
  task.stop();
1856
2335
  this.tasks.delete(id);
1857
2336
  }
2337
+ const timer = this.timers.get(id);
2338
+ if (timer) {
2339
+ clearTimeout(timer);
2340
+ this.timers.delete(id);
2341
+ }
1858
2342
  this.jobs.delete(id);
1859
2343
  this.persist();
1860
2344
  }
@@ -1873,6 +2357,9 @@ var CronScheduler = class {
1873
2357
  return;
1874
2358
  const jobs = await this.store.load();
1875
2359
  for (const job of jobs) {
2360
+ if (job.runAt && Date.parse(job.runAt) <= Date.now()) {
2361
+ continue;
2362
+ }
1876
2363
  this.addJob(job);
1877
2364
  }
1878
2365
  }
@@ -1880,11 +2367,32 @@ var CronScheduler = class {
1880
2367
  for (const task of this.tasks.values()) {
1881
2368
  task.start();
1882
2369
  }
2370
+ for (const job of this.jobs.values()) {
2371
+ if (job.enabled && job.runAt && !job.cron) {
2372
+ this.scheduleOneShot(job);
2373
+ }
2374
+ }
1883
2375
  }
1884
2376
  stop() {
1885
2377
  for (const task of this.tasks.values()) {
1886
2378
  task.stop();
1887
2379
  }
2380
+ for (const timer of this.timers.values()) {
2381
+ clearTimeout(timer);
2382
+ }
2383
+ this.timers.clear();
2384
+ }
2385
+ scheduleOneShot(job) {
2386
+ const delayMs = Date.parse(job.runAt) - Date.now();
2387
+ if (delayMs <= 0)
2388
+ return;
2389
+ const timer = setTimeout(() => {
2390
+ this.timers.delete(job.id);
2391
+ void this.executeHandlers(job).then(() => {
2392
+ this.removeJob(job.id);
2393
+ });
2394
+ }, delayMs);
2395
+ this.timers.set(job.id, timer);
1888
2396
  }
1889
2397
  persist() {
1890
2398
  if (!this.store)
@@ -1939,10 +2447,13 @@ Be concise. Only suggest actions that are clearly needed based on the memory con
1939
2447
  var Heartbeat = class {
1940
2448
  config;
1941
2449
  timer;
2450
+ log;
1942
2451
  constructor(config) {
1943
2452
  this.config = config;
2453
+ this.log = config.logger ?? noopLogger;
1944
2454
  }
1945
2455
  async tick() {
2456
+ this.log.debug("Heartbeat tick");
1946
2457
  const memoryContent = await this.loadMemory();
1947
2458
  const messages = [
1948
2459
  { role: "system", content: HEARTBEAT_PROMPT },
@@ -1957,12 +2468,15 @@ ${memoryContent}`
1957
2468
  const response = await this.config.llm.chat(messages);
1958
2469
  const action = this.parseAction(response.content);
1959
2470
  if (action && action.toLowerCase() !== "none") {
2471
+ this.log.debug(`Heartbeat action: ${action}`);
1960
2472
  await this.config.onAction(action);
2473
+ } else {
2474
+ this.log.debug("Heartbeat: no action needed");
1961
2475
  }
1962
2476
  }
1963
2477
  start() {
1964
2478
  this.timer = setInterval(() => {
1965
- this.tick().catch((err2) => console.error("[augure] Heartbeat error:", err2));
2479
+ this.tick().catch((err2) => this.log.error("Heartbeat error:", err2));
1966
2480
  }, this.config.intervalMs);
1967
2481
  }
1968
2482
  stop() {
@@ -2809,9 +3323,11 @@ var JOB_PREFIX = "skill:";
2809
3323
  var SkillSchedulerBridge = class {
2810
3324
  scheduler;
2811
3325
  manager;
2812
- constructor(scheduler, manager) {
3326
+ log;
3327
+ constructor(scheduler, manager, logger) {
2813
3328
  this.scheduler = scheduler;
2814
3329
  this.manager = manager;
3330
+ this.log = logger ?? noopLogger;
2815
3331
  }
2816
3332
  /** Register cron jobs for all active cron-triggered skills */
2817
3333
  async syncAll() {
@@ -2830,7 +3346,7 @@ var SkillSchedulerBridge = class {
2830
3346
  enabled: true
2831
3347
  });
2832
3348
  } catch (err2) {
2833
- console.error(`[skills] Failed to register cron for ${skill.id}:`, err2);
3349
+ this.log.error(`Failed to register cron for ${skill.id}:`, err2);
2834
3350
  }
2835
3351
  }
2836
3352
  existingJobs.delete(jobId);
@@ -3222,6 +3738,17 @@ var SkillUpdater = class {
3222
3738
  };
3223
3739
  }
3224
3740
  const toVersion = newSkill.meta.version;
3741
+ if (!newSkill.meta.sandbox) {
3742
+ await this.config.manager.save(backup);
3743
+ return {
3744
+ skillId,
3745
+ success: false,
3746
+ rolledBack: true,
3747
+ fromVersion,
3748
+ toVersion,
3749
+ error: "Downloaded skill has sandbox disabled \u2014 rejected for security"
3750
+ };
3751
+ }
3225
3752
  const testResult = await this.config.tester.test(newSkill);
3226
3753
  if (testResult.success) {
3227
3754
  await this.config.manager.save(newSkill);
@@ -3295,10 +3822,11 @@ var VersionChecker = class _VersionChecker {
3295
3822
  };
3296
3823
  }
3297
3824
  }
3298
- /** Compare two semver strings. Returns -1 if a < b, 0 if equal, 1 if a > b */
3825
+ /** Compare two semver strings (MAJOR.MINOR.PATCH only, pre-release suffixes stripped). Returns -1 if a < b, 0 if equal, 1 if a > b */
3299
3826
  static compareVersions(a, b) {
3300
- const pa = a.split(".").map(Number);
3301
- const pb = b.split(".").map(Number);
3827
+ const clean = (v) => v.replace(/^v/, "").split("-")[0];
3828
+ const pa = clean(a).split(".").map(Number);
3829
+ const pb = clean(b).split(".").map(Number);
3302
3830
  for (let i = 0; i < 3; i++) {
3303
3831
  const va = pa[i] ?? 0;
3304
3832
  const vb = pb[i] ?? 0;
@@ -3312,27 +3840,48 @@ var VersionChecker = class _VersionChecker {
3312
3840
  };
3313
3841
 
3314
3842
  // ../core/dist/main.js
3315
- var SYSTEM_PROMPT = `You are Augure, a personal AI assistant. You are proactive, helpful, and concise.
3843
+ var BASE_SYSTEM_PROMPT = `You are Augure, a personal AI assistant. You are proactive, helpful, and concise.
3316
3844
  You speak the same language as the user. You have access to tools and persistent memory.
3317
- Always be direct and actionable.`;
3318
- function resolveLLMClient(config, usage) {
3845
+ Always be direct and actionable.
3846
+
3847
+ ## Your capabilities
3848
+
3849
+ You have access to tools that let you interact with the outside world. Use the datetime tool when the user needs precise time information beyond what is shown in the current date above. Use memory tools to remember and recall information across conversations. Use the schedule tool to create recurring or one-shot tasks.
3850
+
3851
+ If a tool is marked as [NOT CONFIGURED], let the user know it needs to be set up first and share the documentation link from the tool description.`;
3852
+ var SKILLS_PROMPT = `
3853
+ ## Skills
3854
+
3855
+ You can create and manage "skills" \u2014 autonomous code units that run in isolated Docker containers. Skills are powerful: they let you automate tasks, run on a schedule, and self-heal when they break.
3856
+
3857
+ - Use skill_list to see existing skills and their status
3858
+ - Use skill_generate to create a new skill from a natural language description
3859
+ - Use skill_run to execute a skill manually
3860
+ - Use skill_heal to fix a broken skill
3861
+ - Use skill_install to install a skill from the hub
3862
+
3863
+ When a user asks to automate a recurring task (e.g. "check this every morning", "send me a summary daily"), suggest creating a skill with a cron trigger. Skills can also be triggered manually or by events.`;
3864
+ function resolveLLMClient(config, usage, logger) {
3319
3865
  const override = usage !== "default" ? config[usage] : void 0;
3320
3866
  return new OpenRouterClient({
3321
3867
  apiKey: override?.apiKey ?? config.default.apiKey,
3322
3868
  model: override?.model ?? config.default.model,
3323
- maxTokens: override?.maxTokens ?? config.default.maxTokens
3869
+ maxTokens: override?.maxTokens ?? config.default.maxTokens,
3870
+ logger: logger.child("llm")
3324
3871
  });
3325
3872
  }
3326
- async function startAgent(configPath) {
3873
+ async function startAgent(configPath, opts) {
3874
+ const log = createLogger({ level: opts?.debug ? "debug" : "info" });
3327
3875
  const config = await loadConfig(configPath);
3328
- console.log(`[augure] Loaded config: ${config.identity.name}`);
3876
+ log.info(`Loaded config: ${config.identity.name}`);
3877
+ log.debug(`Config path: ${configPath}`);
3329
3878
  let telegram;
3330
- const llm = resolveLLMClient(config.llm, "default");
3331
- const ingestionLLM = resolveLLMClient(config.llm, "ingestion");
3332
- const monitoringLLM = resolveLLMClient(config.llm, "monitoring");
3879
+ const llm = resolveLLMClient(config.llm, "default", log);
3880
+ const ingestionLLM = resolveLLMClient(config.llm, "ingestion", log);
3881
+ const monitoringLLM = resolveLLMClient(config.llm, "monitoring", log);
3333
3882
  const memoryPath = resolve(configPath, "..", config.memory.path);
3334
3883
  const memory = new FileMemoryStore(memoryPath);
3335
- console.log(`[augure] Memory store: ${memoryPath}`);
3884
+ log.info(`Memory store: ${memoryPath}`);
3336
3885
  const retriever = new MemoryRetriever(memory, {
3337
3886
  maxTokens: config.memory.maxRetrievalTokens
3338
3887
  });
@@ -3341,31 +3890,37 @@ async function startAgent(configPath) {
3341
3890
  tools.register(memoryReadTool);
3342
3891
  tools.register(memoryWriteTool);
3343
3892
  tools.register(scheduleTool);
3893
+ tools.register(datetimeTool);
3344
3894
  tools.register(webSearchTool);
3345
3895
  tools.register(httpTool);
3896
+ tools.register(emailTool);
3346
3897
  tools.register(sandboxExecTool);
3347
3898
  tools.register(opencodeTool);
3348
3899
  const jobStorePath = resolve(configPath, "..", "jobs.json");
3349
3900
  const jobStore = new JobStore(jobStorePath);
3350
3901
  const scheduler = new CronScheduler(jobStore);
3351
3902
  await scheduler.loadPersistedJobs();
3352
- console.log(`[augure] Loaded ${scheduler.listJobs().length} persisted jobs`);
3903
+ log.info(`Loaded ${scheduler.listJobs().length} persisted jobs`);
3353
3904
  for (const job of config.scheduler.jobs) {
3354
3905
  if (!scheduler.listJobs().some((j) => j.id === job.id)) {
3355
3906
  scheduler.addJob({ ...job, enabled: true });
3356
3907
  }
3357
3908
  }
3358
3909
  const docker = new Dockerode();
3910
+ const sandboxImage = config.sandbox.image ?? "augure-sandbox:latest";
3911
+ const sandboxLog = log.child("sandbox");
3912
+ await ensureImage(docker, sandboxImage, sandboxLog);
3359
3913
  const pool = new DockerContainerPool(docker, {
3360
- image: config.sandbox.image ?? "augure-sandbox:latest",
3361
- maxTotal: config.security.maxConcurrentSandboxes
3914
+ image: sandboxImage,
3915
+ maxTotal: config.security.maxConcurrentSandboxes,
3916
+ logger: sandboxLog
3362
3917
  });
3363
- console.log(`[augure] Container pool created (max: ${config.security.maxConcurrentSandboxes})`);
3918
+ log.info(`Container pool: max=${config.security.maxConcurrentSandboxes}`);
3364
3919
  let skillManagerRef;
3365
3920
  let skillUpdater;
3366
3921
  if (config.skills) {
3367
3922
  const skillsPath = resolve(configPath, "..", config.skills.path);
3368
- const codingLLM = resolveLLMClient(config.llm, "coding");
3923
+ const codingLLM = resolveLLMClient(config.llm, "coding", log);
3369
3924
  const skillManager = new SkillManager(skillsPath);
3370
3925
  const skillGenerator = new SkillGenerator(codingLLM);
3371
3926
  const skillRunner = new SkillRunner({
@@ -3409,63 +3964,71 @@ async function startAgent(configPath) {
3409
3964
  const updated = updateResults.filter((r) => r.success);
3410
3965
  const failed = updateResults.filter((r) => !r.success);
3411
3966
  if (updated.length > 0) {
3412
- console.log(`[augure] Skills updated: ${updated.map((r) => `${r.skillId} (v${r.fromVersion}\u2192v${r.toVersion})`).join(", ")}`);
3967
+ log.info(`Skills updated: ${updated.map((r) => `${r.skillId} (v${r.fromVersion}\u2192v${r.toVersion})`).join(", ")}`);
3413
3968
  }
3414
3969
  if (failed.length > 0) {
3415
- console.log(`[augure] Skill updates failed: ${failed.map((r) => `${r.skillId}: ${r.error}`).join(", ")}`);
3970
+ log.warn(`Skill updates failed: ${failed.map((r) => `${r.skillId}: ${r.error}`).join(", ")}`);
3416
3971
  }
3417
3972
  } catch (err2) {
3418
- console.error("[augure] Skill update check failed:", err2);
3973
+ log.error("Skill update check failed:", err2);
3419
3974
  }
3420
3975
  }
3421
3976
  skillManagerRef = skillManager;
3422
- console.log(`[augure] Skills system initialized at ${skillsPath}`);
3977
+ log.info(`Skills initialized: ${skillsPath}`);
3423
3978
  }
3424
3979
  tools.setContext({ config, memory, scheduler, pool });
3425
3980
  const auditConfig = config.audit ?? { path: "./logs", enabled: true };
3426
3981
  const auditPath = resolve(configPath, "..", auditConfig.path);
3427
- const audit = auditConfig.enabled ? new FileAuditLogger(auditPath) : new NullAuditLogger();
3428
- console.log(`[augure] Audit logger: ${auditConfig.enabled ? auditPath : "disabled"}`);
3982
+ const audit = auditConfig.enabled ? new FileAuditLogger(auditPath, log.child("audit")) : new NullAuditLogger();
3983
+ log.info(`Audit: ${auditConfig.enabled ? auditPath : "disabled"}`);
3429
3984
  let personaResolver;
3430
3985
  if (config.persona) {
3431
3986
  const personaPath = resolve(configPath, "..", config.persona.path);
3432
3987
  personaResolver = new PersonaResolver(personaPath);
3433
3988
  await personaResolver.loadAll();
3434
- console.log(`[augure] Personas loaded from ${personaPath}`);
3989
+ log.info(`Personas: ${personaPath}`);
3435
3990
  }
3436
- if (config.updates?.cli?.enabled !== false) {
3991
+ let cliVersion;
3992
+ try {
3437
3993
  const require3 = createRequire(import.meta.url);
3438
- const { version: version2 } = require3("augure/package.json");
3994
+ const pkg = require3("augure/package.json");
3995
+ cliVersion = pkg.version;
3996
+ } catch {
3997
+ }
3998
+ if (cliVersion && config.updates?.cli?.enabled !== false) {
3439
3999
  const versionChecker = new VersionChecker({
3440
- currentVersion: version2,
3441
- packageName: "augure",
3442
- githubRepo: "FaureAlexis/augure"
4000
+ currentVersion: cliVersion,
4001
+ packageName: "augure"
3443
4002
  });
3444
4003
  const versionResult = await versionChecker.check();
3445
4004
  if (versionResult.updateAvailable) {
3446
- console.log(`[augure] Update available: v${versionResult.latestVersion} (current: v${versionResult.currentVersion}). Run: npm update -g augure`);
4005
+ log.warn(`Update available: v${versionResult.latestVersion} (current: v${versionResult.currentVersion}). Run: npm update -g augure`);
3447
4006
  }
3448
4007
  }
3449
4008
  const guard = new ContextGuard({
3450
4009
  maxContextTokens: 2e5,
3451
4010
  reservedForOutput: config.llm.default.maxTokens ?? 8192
3452
4011
  });
4012
+ const systemPrompt = config.skills ? BASE_SYSTEM_PROMPT + SKILLS_PROMPT : BASE_SYSTEM_PROMPT;
3453
4013
  const agent = new Agent({
3454
4014
  llm,
3455
4015
  tools,
3456
- systemPrompt: SYSTEM_PROMPT,
4016
+ systemPrompt,
3457
4017
  memoryContent: "",
3458
4018
  retriever,
3459
4019
  ingester,
3460
4020
  audit,
3461
4021
  guard,
3462
- modelName: config.llm.default.model
4022
+ modelName: config.llm.default.model,
4023
+ logger: log.child("agent")
3463
4024
  });
3464
4025
  if (config.channels.telegram?.enabled) {
4026
+ const telegramLog = log.child("telegram");
3465
4027
  telegram = new TelegramChannel({
3466
4028
  botToken: config.channels.telegram.botToken,
3467
4029
  allowedUsers: config.channels.telegram.allowedUsers,
3468
- rejectMessage: config.channels.telegram.rejectMessage
4030
+ rejectMessage: config.channels.telegram.rejectMessage,
4031
+ logger: telegramLog
3469
4032
  });
3470
4033
  const tg = telegram;
3471
4034
  const commandCtx = {
@@ -3475,7 +4038,7 @@ async function startAgent(configPath) {
3475
4038
  skillManager: skillManagerRef
3476
4039
  };
3477
4040
  tg.onMessage(async (msg) => {
3478
- console.log(`[augure] Message from ${msg.userId}: ${msg.text}`);
4041
+ log.info(`Message from ${msg.userId}: ${msg.text}`);
3479
4042
  try {
3480
4043
  const cmdResult = await handleCommand(msg.text, commandCtx);
3481
4044
  if (cmdResult.handled) {
@@ -3498,7 +4061,7 @@ async function startAgent(configPath) {
3498
4061
  replyTo: msg.id
3499
4062
  });
3500
4063
  } catch (err2) {
3501
- console.error("[augure] Error handling message:", err2);
4064
+ log.error("Error handling message:", err2);
3502
4065
  await tg.send({
3503
4066
  channelType: "telegram",
3504
4067
  userId: msg.userId,
@@ -3507,15 +4070,16 @@ async function startAgent(configPath) {
3507
4070
  }
3508
4071
  });
3509
4072
  await tg.start();
3510
- console.log("[augure] Telegram bot started. Waiting for messages...");
4073
+ log.info("Telegram bot started");
3511
4074
  }
3512
4075
  const heartbeatIntervalMs = parseInterval(config.scheduler.heartbeatInterval);
3513
4076
  const heartbeat = new Heartbeat({
3514
4077
  llm: monitoringLLM,
3515
4078
  memory,
3516
4079
  intervalMs: heartbeatIntervalMs,
4080
+ logger: log.child("heartbeat"),
3517
4081
  onAction: async (action) => {
3518
- console.log(`[augure] Heartbeat action: ${action}`);
4082
+ log.info(`Heartbeat action: ${action}`);
3519
4083
  const response = await agent.handleMessage({
3520
4084
  id: `heartbeat-${Date.now()}`,
3521
4085
  channelType: "system",
@@ -3523,39 +4087,59 @@ async function startAgent(configPath) {
3523
4087
  text: `[Heartbeat] ${action}`,
3524
4088
  timestamp: /* @__PURE__ */ new Date()
3525
4089
  });
3526
- console.log(`[augure] Heartbeat response: ${response}`);
4090
+ log.debug(`Heartbeat response: ${response.slice(0, 200)}`);
3527
4091
  }
3528
4092
  });
4093
+ scheduler.onJobTrigger(async (job) => {
4094
+ log.info(`Job triggered: ${job.id}`);
4095
+ const response = await agent.handleMessage({
4096
+ id: `job-${job.id}-${Date.now()}`,
4097
+ channelType: "system",
4098
+ userId: "system",
4099
+ text: job.prompt,
4100
+ timestamp: /* @__PURE__ */ new Date()
4101
+ });
4102
+ if (telegram && config.channels.telegram?.enabled) {
4103
+ const userId = config.channels.telegram.allowedUsers[0];
4104
+ if (userId !== void 0) {
4105
+ await telegram.send({
4106
+ channelType: "telegram",
4107
+ userId: String(userId),
4108
+ text: response
4109
+ });
4110
+ }
4111
+ }
4112
+ log.debug(`Job ${job.id} completed`);
4113
+ });
3529
4114
  scheduler.start();
3530
4115
  heartbeat.start();
3531
- console.log(`[augure] Scheduler started with ${scheduler.listJobs().length} jobs. Heartbeat every ${config.scheduler.heartbeatInterval}.`);
4116
+ log.info(`Scheduler started: ${scheduler.listJobs().length} jobs, heartbeat every ${config.scheduler.heartbeatInterval}`);
4117
+ const updateTimers = [];
3532
4118
  if (skillUpdater && config.updates?.skills?.checkInterval) {
4119
+ const su = skillUpdater;
3533
4120
  const skillCheckMs = parseInterval(config.updates.skills.checkInterval);
3534
- setInterval(async () => {
4121
+ updateTimers.push(setInterval(async () => {
3535
4122
  try {
3536
- const results = await skillUpdater.checkAndApply();
4123
+ const results = await su.checkAndApply();
3537
4124
  for (const r of results) {
3538
4125
  if (r.success) {
3539
- console.log(`[augure] Skill auto-updated: ${r.skillId} v${r.fromVersion}\u2192v${r.toVersion}`);
4126
+ log.info(`Skill auto-updated: ${r.skillId} v${r.fromVersion}\u2192v${r.toVersion}`);
3540
4127
  } else if (r.rolledBack) {
3541
- console.log(`[augure] Skill update rolled back: ${r.skillId} - ${r.error}`);
4128
+ log.warn(`Skill update rolled back: ${r.skillId} - ${r.error}`);
3542
4129
  }
3543
4130
  }
3544
4131
  } catch (err2) {
3545
- console.error("[augure] Periodic skill update check failed:", err2);
4132
+ log.error("Periodic skill update check failed:", err2);
3546
4133
  }
3547
- }, skillCheckMs);
4134
+ }, skillCheckMs));
3548
4135
  }
3549
- if (config.updates?.cli?.enabled !== false && config.channels.telegram?.enabled) {
4136
+ if (cliVersion && config.updates?.cli?.enabled !== false && config.channels.telegram?.enabled) {
3550
4137
  const cliCheckMs = parseInterval(config.updates?.cli?.checkInterval ?? "24h");
3551
- const require3 = createRequire(import.meta.url);
3552
- const { version: version2 } = require3("augure/package.json");
3553
4138
  const versionChecker = new VersionChecker({
3554
- currentVersion: version2,
3555
- packageName: "augure",
3556
- githubRepo: "FaureAlexis/augure"
4139
+ currentVersion: cliVersion,
4140
+ packageName: "augure"
3557
4141
  });
3558
- setInterval(async () => {
4142
+ updateTimers.push(setInterval(async () => {
3559
4143
  try {
3560
4144
  const result = await versionChecker.check();
3561
4145
  if (result.updateAvailable && telegram) {
@@ -3570,35 +4154,27 @@ Run: \`npm update -g augure\``
3570
4154
  }
3571
4155
  }
3572
4156
  } catch (err2) {
3573
- console.error("[augure] CLI version check failed:", err2);
4157
+ log.error("CLI version check failed:", err2);
3574
4158
  }
3575
- }, cliCheckMs);
4159
+ }, cliCheckMs));
3576
4160
  }
3577
4161
  const shutdown = async () => {
3578
- console.log("\n[augure] Shutting down...");
4162
+ log.info("Shutting down...");
4163
+ for (const timer of updateTimers)
4164
+ clearInterval(timer);
3579
4165
  heartbeat.stop();
3580
4166
  scheduler.stop();
3581
4167
  if (telegram)
3582
4168
  await telegram.stop();
3583
4169
  await pool.destroyAll();
3584
4170
  await audit.close();
3585
- console.log("[augure] All containers destroyed");
4171
+ log.info("All containers destroyed");
3586
4172
  process.exit(0);
3587
4173
  };
3588
4174
  process.on("SIGINT", shutdown);
3589
4175
  process.on("SIGTERM", shutdown);
3590
4176
  }
3591
4177
 
3592
- // src/colors.ts
3593
- import { styleText } from "util";
3594
- var brand = (s) => styleText("yellow", s);
3595
- var ok = (s) => styleText("green", s);
3596
- var err = (s) => styleText("red", s);
3597
- var dim = (s) => styleText("dim", s);
3598
- var bold = (s) => styleText("bold", s);
3599
- var cyan = (s) => styleText("cyan", s);
3600
- var prefix = brand("\u25B2 augure");
3601
-
3602
4178
  // src/commands/start.ts
3603
4179
  var startCommand = defineCommand({
3604
4180
  meta: {
@@ -3616,21 +4192,28 @@ var startCommand = defineCommand({
3616
4192
  type: "string",
3617
4193
  description: "Path to .env file",
3618
4194
  alias: "e"
4195
+ },
4196
+ debug: {
4197
+ type: "boolean",
4198
+ description: "Enable debug logging",
4199
+ alias: "d",
4200
+ default: false
3619
4201
  }
3620
4202
  },
3621
4203
  async run({ args }) {
3622
4204
  const configPath = resolve2(args.config);
4205
+ const log = createLogger({ level: args.debug ? "debug" : "info" });
3623
4206
  const envPath = args.env ? resolve2(args.env) : join6(dirname4(configPath), ".env");
3624
4207
  try {
3625
4208
  process.loadEnvFile(envPath);
3626
- console.log(`${prefix} Loaded env from ${dim(envPath)}`);
4209
+ log.debug(`Loaded env from ${envPath}`);
3627
4210
  } catch {
3628
4211
  }
3629
- console.log(`${prefix} Starting with config: ${dim(configPath)}`);
4212
+ log.info(`Starting with config: ${configPath}`);
3630
4213
  try {
3631
- await startAgent(configPath);
4214
+ await startAgent(configPath, { debug: args.debug });
3632
4215
  } catch (e) {
3633
- console.error(`${prefix} ${err("Fatal error:")} ${e instanceof Error ? e.message : e}`);
4216
+ log.error("Fatal:", e instanceof Error ? e.message : String(e));
3634
4217
  process.exit(1);
3635
4218
  }
3636
4219
  }
@@ -3640,6 +4223,18 @@ var startCommand = defineCommand({
3640
4223
  import { defineCommand as defineCommand2 } from "citty";
3641
4224
  import { writeFile as writeFile5, access as access3 } from "fs/promises";
3642
4225
  import { resolve as resolve3 } from "path";
4226
+
4227
+ // src/colors.ts
4228
+ import { styleText as styleText2 } from "util";
4229
+ var brand = (s) => styleText2("yellow", s);
4230
+ var ok = (s) => styleText2("green", s);
4231
+ var err = (s) => styleText2("red", s);
4232
+ var dim = (s) => styleText2("dim", s);
4233
+ var bold = (s) => styleText2("bold", s);
4234
+ var cyan = (s) => styleText2("cyan", s);
4235
+ var prefix = brand("\u25B2 augure");
4236
+
4237
+ // src/commands/init.ts
3643
4238
  var CONFIG_TEMPLATE = `{
3644
4239
  // Identity
3645
4240
  identity: {