mixdog 0.9.66 → 0.9.68

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 (45) hide show
  1. package/package.json +1 -1
  2. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +3 -0
  3. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +5 -0
  4. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +20 -2
  5. package/src/runtime/channels/lib/config.mjs +13 -5
  6. package/src/runtime/channels/lib/owned-runtime.mjs +65 -5
  7. package/src/runtime/channels/lib/scheduler.mjs +7 -15
  8. package/src/runtime/channels/lib/status-snapshot.mjs +6 -30
  9. package/src/runtime/channels/lib/tool-dispatch.mjs +6 -1
  10. package/src/runtime/channels/lib/webhook/relay-tunnel.mjs +203 -0
  11. package/src/runtime/channels/lib/webhook.mjs +46 -246
  12. package/src/runtime/channels/lib/worker-main.mjs +45 -92
  13. package/src/runtime/shared/automation-attachments.mjs +72 -0
  14. package/src/runtime/shared/automation-workflow.mjs +41 -0
  15. package/src/runtime/shared/config.mjs +0 -9
  16. package/src/runtime/shared/schedule-session-run.mjs +10 -1
  17. package/src/runtime/shared/schedules-db.mjs +18 -3
  18. package/src/runtime/shared/time-format.mjs +56 -0
  19. package/src/runtime/shared/tool-card-model.mjs +740 -0
  20. package/src/runtime/shared/webhook-session-run.mjs +57 -0
  21. package/src/runtime/shared/webhooks-db.mjs +19 -3
  22. package/src/session-runtime/channel-config-api.mjs +13 -1
  23. package/src/session-runtime/lifecycle-api.mjs +12 -0
  24. package/src/session-runtime/prewarm.mjs +13 -7
  25. package/src/session-runtime/runtime-core.mjs +127 -22
  26. package/src/session-runtime/session-text.mjs +6 -0
  27. package/src/session-runtime/settings-api.mjs +0 -12
  28. package/src/standalone/channel-admin.mjs +71 -22
  29. package/src/standalone/channel-daemon-client.mjs +5 -1
  30. package/src/standalone/channel-daemon-transport.mjs +112 -20
  31. package/src/standalone/channel-daemon.mjs +6 -4
  32. package/src/tui/App.jsx +2 -47
  33. package/src/tui/app/channel-pickers.mjs +8 -173
  34. package/src/tui/app/doctor.mjs +0 -1
  35. package/src/tui/app/slash-commands.mjs +1 -3
  36. package/src/tui/app/slash-dispatch.mjs +3 -3
  37. package/src/tui/components/ToolExecution.jsx +47 -196
  38. package/src/tui/components/tool-execution/surface-detail.mjs +33 -394
  39. package/src/tui/components/tool-execution/text-format.mjs +27 -104
  40. package/src/tui/dist/index.mjs +340 -346
  41. package/src/tui/engine/live-share.mjs +9 -0
  42. package/src/tui/engine/session-api-ext.mjs +32 -16
  43. package/src/tui/engine.mjs +14 -1
  44. package/src/tui/time-format.mjs +4 -51
  45. package/src/runtime/channels/lib/webhook/ngrok.mjs +0 -181
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Resolve a stored automation workflow id (schedule/webhook row) into
3
+ * createSession options: the workflow summary meta plus the WORKFLOW.md
4
+ * context block, exactly like a desktop New task session gets for the
5
+ * active workflow. An empty/unknown id resolves to the default pack via
6
+ * loadWorkflowPack's fallback; resolution failures degrade to no workflow
7
+ * context (the session still runs with the base lead ruleset).
8
+ */
9
+ import { createWorkflowHelpers } from '../../session-runtime/workflow.mjs';
10
+ import { STANDALONE_ROOT, STANDALONE_DATA_DIR } from '../../session-runtime/runtime-paths.mjs';
11
+ import { readMarkdownDocument, normalizeAgentPermissionOrNone } from './markdown-frontmatter.mjs';
12
+
13
+ let _helpers = null;
14
+ function helpers() {
15
+ if (!_helpers) {
16
+ _helpers = createWorkflowHelpers({
17
+ rootDir: STANDALONE_ROOT,
18
+ dataDir: STANDALONE_DATA_DIR,
19
+ readMarkdownDocument,
20
+ normalizeAgentPermissionOrNone,
21
+ });
22
+ }
23
+ return _helpers;
24
+ }
25
+
26
+ /** { workflow, workflowContext } createSession opts for a workflow id, or {} when unset/unresolvable. */
27
+ export function automationWorkflowOpts(workflowId) {
28
+ const id = String(workflowId || '').trim();
29
+ if (!id) return {};
30
+ try {
31
+ const h = helpers();
32
+ const pack = h.loadWorkflowPack(undefined, id);
33
+ if (!pack) return {};
34
+ return {
35
+ workflow: h.workflowSummary(pack),
36
+ workflowContext: h.workflowContextBlock({ workflow: { active: id } }, undefined),
37
+ };
38
+ } catch {
39
+ return {};
40
+ }
41
+ }
@@ -437,7 +437,6 @@ function getCapabilities() {
437
437
  export const SECRET_ACCOUNTS = Object.freeze({
438
438
  discordToken: 'discord.token',
439
439
  telegramToken: 'telegram.token',
440
- webhookAuth: 'webhook.authtoken',
441
440
  agentApiKey: (provider) => `agent.${provider}.apiKey`,
442
441
  openaiUsageSessionKey: 'agent.openai.usageSessionKey',
443
442
  opencodeGoAuthCookie: 'agent.opencode-go.authCookie',
@@ -497,14 +496,6 @@ export function getTelegramToken() {
497
496
  return _readSecret(SECRET_ACCOUNTS.telegramToken)
498
497
  }
499
498
 
500
- /**
501
- * Returns the ngrok/webhook authtoken.
502
- * Priority: MIXDOG_WEBHOOK_AUTHTOKEN → keychain('webhook.authtoken') → null
503
- */
504
- export function getWebhookAuthtoken() {
505
- return _readSecret(SECRET_ACCOUNTS.webhookAuth)
506
- }
507
-
508
499
  export function getOpenAIUsageSessionKey() {
509
500
  return process.env.OPENAI_USAGE_SESSION_KEY
510
501
  || process.env.OPENAI_DASHBOARD_SESSION_KEY
@@ -10,6 +10,8 @@ import { loadConfig } from '../agent/orchestrator/config.mjs';
10
10
  import { createSession } from '../agent/orchestrator/session/manager/session-lifecycle.mjs';
11
11
  import { askSession } from '../agent/orchestrator/session/manager/ask-session.mjs';
12
12
  import { parseScheduleModelRef } from './schedule-model-ref.mjs';
13
+ import { automationWorkflowOpts } from './automation-workflow.mjs';
14
+ import { automationPromptContent } from './automation-attachments.mjs';
13
15
 
14
16
  /** Resolve schedule.model into a {provider,model,effort?,fast?} route. */
15
17
  function scheduleRouteFromModelRef(modelRef, config = null) {
@@ -43,6 +45,9 @@ export async function runScheduleSession(schedule, { config = null, prompt: prom
43
45
  }
44
46
  const route = scheduleRouteFromModelRef(schedule.model, config);
45
47
  const cwd = schedule.cwd ? String(schedule.cwd) : null;
48
+ // Every fire is a fresh New task (user decision): one prompt, the
49
+ // schedule's model/workflow/project, a brand-new session in the sidebar
50
+ // Automations section (newest per name wins there).
46
51
  const session = createSession({
47
52
  provider: route.provider,
48
53
  model: route.model,
@@ -51,13 +56,17 @@ export async function runScheduleSession(schedule, { config = null, prompt: prom
51
56
  owner: 'user',
52
57
  sourceType: 'schedule',
53
58
  sourceName: schedule.name,
59
+ sourceDelivery: schedule.delivery || null,
54
60
  ...(cwd ? { cwd } : {}),
55
61
  desktopSession: cwd
56
62
  ? { classification: 'project', projectPath: cwd }
57
63
  : { classification: 'task', projectPath: null },
64
+ ...automationWorkflowOpts(schedule.workflow),
58
65
  });
59
66
  // The user message is the schedule's instructions verbatim: the desktop
60
67
  // title/preview derive from it, so no "[Scheduled task: …]" header noise.
61
- const result = await askSession(session.id, prompt, null, null, cwd || undefined);
68
+ // Stored attachments ride along as composer-style content parts.
69
+ const content = automationPromptContent(prompt, schedule.attachments);
70
+ const result = await askSession(session.id, content, null, null, cwd || undefined);
62
71
  return { sessionId: session.id, result: String(result?.content || '') };
63
72
  }
@@ -33,6 +33,9 @@ CREATE TABLE IF NOT EXISTS scheduler.schedules (
33
33
  channel_id text,
34
34
  model text,
35
35
  cwd text,
36
+ workflow text,
37
+ attachments jsonb,
38
+ delivery text,
36
39
  prompt text NOT NULL,
37
40
  enabled boolean NOT NULL DEFAULT true,
38
41
  status text NOT NULL DEFAULT 'active' CHECK (status IN ('active','done')),
@@ -45,6 +48,9 @@ CREATE TABLE IF NOT EXISTS scheduler.schedules (
45
48
  CONSTRAINT schedules_when_xor CHECK ((when_at IS NOT NULL) <> (when_cron IS NOT NULL))
46
49
  );
47
50
  ALTER TABLE scheduler.schedules ADD COLUMN IF NOT EXISTS cwd text;
51
+ ALTER TABLE scheduler.schedules ADD COLUMN IF NOT EXISTS workflow text;
52
+ ALTER TABLE scheduler.schedules ADD COLUMN IF NOT EXISTS attachments jsonb;
53
+ ALTER TABLE scheduler.schedules ADD COLUMN IF NOT EXISTS delivery text;
48
54
  `;
49
55
 
50
56
  async function getDb(dataDir = resolvePluginData()) {
@@ -82,6 +88,9 @@ function rowToDef(row) {
82
88
  channelId: row.channel_id,
83
89
  model: row.model,
84
90
  cwd: row.cwd,
91
+ workflow: row.workflow,
92
+ attachments: row.attachments || null,
93
+ delivery: row.delivery || null,
85
94
  prompt: row.prompt,
86
95
  enabled: row.enabled,
87
96
  status: row.status,
@@ -94,7 +103,7 @@ function rowToDef(row) {
94
103
  };
95
104
  }
96
105
 
97
- const COLS = 'name, description, when_at, when_cron, timezone, target, channel_id, model, cwd, prompt, enabled, status, last_fired_at, next_fire_at, deferred_until, skipped_until, created_at, updated_at';
106
+ const COLS = 'name, description, when_at, when_cron, timezone, target, channel_id, model, cwd, workflow, attachments, delivery, prompt, enabled, status, last_fired_at, next_fire_at, deferred_until, skipped_until, created_at, updated_at';
98
107
 
99
108
  // ---------------------------------------------------------------------------
100
109
  // Public API
@@ -130,6 +139,9 @@ export async function upsertSchedule(def, { dataDir } = {}) {
130
139
  def.channelId ?? null,
131
140
  def.model ?? null,
132
141
  def.cwd ?? null,
142
+ def.workflow ?? null,
143
+ def.attachments ? JSON.stringify(def.attachments) : null,
144
+ def.delivery ?? null,
133
145
  def.prompt,
134
146
  def.enabled ?? true,
135
147
  def.status ?? 'active',
@@ -137,8 +149,8 @@ export async function upsertSchedule(def, { dataDir } = {}) {
137
149
  ];
138
150
  const { rows } = await db.query(
139
151
  `INSERT INTO scheduler.schedules
140
- (name, description, when_at, when_cron, timezone, target, channel_id, model, cwd, prompt, enabled, status, next_fire_at)
141
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13)
152
+ (name, description, when_at, when_cron, timezone, target, channel_id, model, cwd, workflow, attachments, delivery, prompt, enabled, status, next_fire_at)
153
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16)
142
154
  ON CONFLICT (name) DO UPDATE SET
143
155
  description = EXCLUDED.description,
144
156
  when_at = EXCLUDED.when_at,
@@ -148,6 +160,9 @@ export async function upsertSchedule(def, { dataDir } = {}) {
148
160
  channel_id = EXCLUDED.channel_id,
149
161
  model = EXCLUDED.model,
150
162
  cwd = EXCLUDED.cwd,
163
+ workflow = EXCLUDED.workflow,
164
+ attachments = EXCLUDED.attachments,
165
+ delivery = EXCLUDED.delivery,
151
166
  prompt = EXCLUDED.prompt,
152
167
  enabled = EXCLUDED.enabled,
153
168
  next_fire_at = EXCLUDED.next_fire_at,
@@ -0,0 +1,56 @@
1
+ /**
2
+ * runtime/shared/time-format.mjs — compact elapsed-time labels shared by every
3
+ * surface (TUI, terminal, desktop renderer). Moved verbatim from
4
+ * src/tui/time-format.mjs so tool-card detail rows ("Running · 12s") derive
5
+ * from ONE formatter; the TUI module re-exports from here.
6
+ *
7
+ * Examples:
8
+ * 42s
9
+ * 9m 23s
10
+ * 1h 2m 3s
11
+ * 1d 3h 20m
12
+ */
13
+ export function formatDuration(ms, options = {}) {
14
+ if (!Number.isFinite(Number(ms))) return '';
15
+ const value = Math.max(0, Number(ms) || 0);
16
+ if (value < 60_000) {
17
+ if (value < 1_000) return '';
18
+ return `${Math.floor(value / 1000)}s`;
19
+ }
20
+
21
+ let days = Math.floor(value / 86_400_000);
22
+ let hours = Math.floor((value % 86_400_000) / 3_600_000);
23
+ let minutes = Math.floor((value % 3_600_000) / 60_000);
24
+ const seconds = Math.floor((value % 60_000) / 1000);
25
+
26
+ if (options.mostSignificantOnly) {
27
+ if (days > 0) return `${days}d`;
28
+ if (hours > 0) return `${hours}h`;
29
+ if (minutes > 0) return `${minutes}m`;
30
+ return `${seconds}s`;
31
+ }
32
+
33
+ const hide = options.hideTrailingZeros;
34
+ if (days > 0) {
35
+ if (hide && hours === 0 && minutes === 0) return `${days}d`;
36
+ if (hide && minutes === 0) return `${days}d ${hours}h`;
37
+ return `${days}d ${hours}h ${minutes}m`;
38
+ }
39
+ if (hours > 0) {
40
+ if (hide && minutes === 0 && seconds === 0) return `${hours}h`;
41
+ if (hide && seconds === 0) return `${hours}h ${minutes}m`;
42
+ return `${hours}h ${minutes}m ${seconds}s`;
43
+ }
44
+ if (minutes > 0) {
45
+ if (hide && seconds === 0) return `${minutes}m`;
46
+ return `${minutes}m ${seconds}s`;
47
+ }
48
+ return `${seconds}s`;
49
+ }
50
+
51
+ export function formatElapsed(ms) {
52
+ const n = Math.max(0, Number(ms || 0));
53
+ if (!Number.isFinite(n) || n <= 0) return '';
54
+ if (n < 1000) return '';
55
+ return formatDuration(n);
56
+ }