@fitlab-ai/agent-infra 0.8.2 → 0.8.3

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 (119) hide show
  1. package/dist/bin/cli.js +1 -1
  2. package/dist/lib/cp.js +1 -1
  3. package/dist/lib/decide.js +2 -2
  4. package/dist/lib/defaults.json +1 -0
  5. package/dist/lib/init.js +7 -7
  6. package/dist/lib/merge.js +1 -1
  7. package/dist/lib/prompt.js +1 -1
  8. package/dist/lib/run/index.js +7 -7
  9. package/dist/lib/run/prompt.js +1 -1
  10. package/dist/lib/sandbox/capture.js +5 -5
  11. package/dist/lib/sandbox/clipboard/bridge.js +48 -13
  12. package/dist/lib/sandbox/clipboard/index.js +5 -6
  13. package/dist/lib/sandbox/clipboard/linux.js +91 -0
  14. package/dist/lib/sandbox/clipboard/paths.js +1 -1
  15. package/dist/lib/sandbox/clipboard/win32.js +144 -0
  16. package/dist/lib/sandbox/commands/create.js +67 -65
  17. package/dist/lib/sandbox/commands/enter.js +9 -9
  18. package/dist/lib/sandbox/commands/list-running.js +2 -2
  19. package/dist/lib/sandbox/commands/ls.js +7 -7
  20. package/dist/lib/sandbox/commands/prune.js +8 -8
  21. package/dist/lib/sandbox/commands/rebuild.js +38 -50
  22. package/dist/lib/sandbox/commands/refresh.js +2 -2
  23. package/dist/lib/sandbox/commands/rm.js +11 -11
  24. package/dist/lib/sandbox/commands/show.js +4 -4
  25. package/dist/lib/sandbox/commands/start.js +4 -4
  26. package/dist/lib/sandbox/commands/vm.js +4 -4
  27. package/dist/lib/sandbox/config.js +10 -4
  28. package/dist/lib/sandbox/constants.js +4 -1
  29. package/dist/lib/sandbox/credentials.js +1 -1
  30. package/dist/lib/sandbox/dotfiles.js +1 -1
  31. package/dist/lib/sandbox/engine.js +3 -3
  32. package/dist/lib/sandbox/engines/index.js +5 -5
  33. package/dist/lib/sandbox/engines/wsl2-paths.js +1 -1
  34. package/dist/lib/sandbox/image-build.js +80 -0
  35. package/dist/lib/sandbox/image-prune.js +2 -2
  36. package/dist/lib/sandbox/index.js +10 -10
  37. package/dist/lib/sandbox/managed-fs.js +1 -1
  38. package/dist/lib/sandbox/readme-scaffold.js +1 -1
  39. package/dist/lib/sandbox/tools.js +2 -2
  40. package/dist/lib/server/adapters/feishu/index.js +10 -3
  41. package/dist/lib/server/adapters/feishu/renderer.js +88 -0
  42. package/dist/lib/server/adapters/feishu/transport.js +2 -13
  43. package/dist/lib/server/daemon.js +64 -43
  44. package/dist/lib/server/display.js +83 -0
  45. package/dist/lib/server/index.js +2 -2
  46. package/dist/lib/server/process-control.js +2 -2
  47. package/dist/lib/server/protocol.js +2 -2
  48. package/dist/lib/server/streamer.js +5 -4
  49. package/dist/lib/task/commands/cat.js +2 -2
  50. package/dist/lib/task/commands/decisions.js +4 -4
  51. package/dist/lib/task/commands/files.js +3 -3
  52. package/dist/lib/task/commands/grep.js +3 -3
  53. package/dist/lib/task/commands/issue-body.js +4 -4
  54. package/dist/lib/task/commands/log.js +5 -5
  55. package/dist/lib/task/commands/ls.js +3 -3
  56. package/dist/lib/task/commands/show.js +1 -1
  57. package/dist/lib/task/commands/status.js +71 -25
  58. package/dist/lib/task/index.js +9 -9
  59. package/dist/lib/task/resolve-ref.js +1 -1
  60. package/dist/lib/task/workflow-warnings.js +94 -0
  61. package/dist/lib/update.js +4 -4
  62. package/lib/defaults.json +1 -0
  63. package/lib/sandbox/clipboard/bridge.ts +50 -8
  64. package/lib/sandbox/clipboard/index.ts +5 -6
  65. package/lib/sandbox/clipboard/linux.ts +124 -0
  66. package/lib/sandbox/clipboard/win32.ts +173 -0
  67. package/lib/sandbox/commands/create.ts +84 -64
  68. package/lib/sandbox/commands/rebuild.ts +42 -54
  69. package/lib/sandbox/config.ts +13 -1
  70. package/lib/sandbox/constants.ts +4 -0
  71. package/lib/sandbox/image-build.ts +134 -0
  72. package/lib/server/adapters/_contract.ts +3 -0
  73. package/lib/server/adapters/feishu/index.ts +11 -3
  74. package/lib/server/adapters/feishu/renderer.ts +99 -0
  75. package/lib/server/adapters/feishu/transport.ts +5 -18
  76. package/lib/server/daemon.ts +74 -38
  77. package/lib/server/display.ts +136 -0
  78. package/lib/server/streamer.ts +5 -4
  79. package/lib/task/commands/log.ts +3 -3
  80. package/lib/task/commands/status.ts +102 -21
  81. package/lib/task/workflow-warnings.ts +121 -0
  82. package/package.json +2 -2
  83. package/templates/.agents/rules/create-issue.github.en.md +3 -3
  84. package/templates/.agents/rules/create-issue.github.zh-CN.md +3 -3
  85. package/templates/.agents/rules/issue-pr-commands.github.en.md +7 -2
  86. package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +7 -2
  87. package/templates/.agents/rules/issue-sync.github.en.md +13 -0
  88. package/templates/.agents/rules/issue-sync.github.zh-CN.md +13 -0
  89. package/templates/.agents/rules/next-step-output.en.md +15 -1
  90. package/templates/.agents/rules/next-step-output.zh-CN.md +15 -1
  91. package/templates/.agents/rules/pr-sync.github.en.md +17 -2
  92. package/templates/.agents/rules/pr-sync.github.zh-CN.md +17 -2
  93. package/templates/.agents/rules/task-management.en.md +1 -1
  94. package/templates/.agents/rules/task-management.zh-CN.md +1 -1
  95. package/templates/.agents/scripts/platform-adapters/platform-sync.github.js +57 -0
  96. package/templates/.agents/scripts/validate-artifact.js +120 -0
  97. package/templates/.agents/scripts/workflow-warnings.js +290 -0
  98. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +118 -0
  99. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +118 -0
  100. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +49 -0
  101. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +49 -0
  102. package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +48 -0
  103. package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +48 -0
  104. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +60 -0
  105. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +87 -0
  106. package/templates/.agents/skills/create-pr/SKILL.en.md +2 -0
  107. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +2 -0
  108. package/templates/.agents/skills/create-task/SKILL.en.md +4 -1
  109. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +4 -1
  110. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +7 -0
  111. package/templates/.agents/templates/task.en.md +7 -0
  112. package/templates/.agents/templates/task.zh-CN.md +7 -0
  113. package/templates/.claude/commands/complete-manual-validation.en.md +9 -0
  114. package/templates/.claude/commands/complete-manual-validation.zh-CN.md +9 -0
  115. package/templates/.gemini/commands/_project_/complete-manual-validation.en.toml +8 -0
  116. package/templates/.gemini/commands/_project_/complete-manual-validation.zh-CN.toml +8 -0
  117. package/templates/.github/workflows/metadata-sync.yml +5 -0
  118. package/templates/.opencode/commands/complete-manual-validation.en.md +11 -0
  119. package/templates/.opencode/commands/complete-manual-validation.zh-CN.md +11 -0
@@ -0,0 +1,80 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { detectEngine } from './engine.js';
3
+ import { resolveBuildUid } from './engines/native.js';
4
+ import { toEnginePath } from './engines/wsl2-paths.js';
5
+ import { sandboxImageConfigLabel, sandboxImageRefreshLabel, sandboxLabel } from './constants.js';
6
+ import { runEngine, runSafeEngine } from './shell.js';
7
+ import { imageSignatureFields, toolNpmPackagesArg, toolShellInstallScriptBase64 } from './tools.js';
8
+ export function buildImageSignature(preparedDockerfile, tools) {
9
+ return createHash('sha256')
10
+ .update(JSON.stringify({
11
+ dockerfile: preparedDockerfile.signature,
12
+ tools: imageSignatureFields(tools)
13
+ }))
14
+ .digest('hex')
15
+ .slice(0, 12);
16
+ }
17
+ export function buildSandboxImageArgs(config, tools, dockerfilePath, imageSignature, { engine, runFn = runEngine, runSafeFn = runSafeEngine, env = process.env, refresh = false, lastRefresh } = {}) {
18
+ const selectedEngine = engine ?? detectEngine({ engine: config.engine });
19
+ const { uid: hostUid, gid: hostGid } = resolveBuildUid({
20
+ engine: selectedEngine,
21
+ runFn,
22
+ runSafeFn,
23
+ env
24
+ });
25
+ const args = [
26
+ 'build',
27
+ '-t',
28
+ config.imageName,
29
+ '--build-arg',
30
+ `HOST_UID=${hostUid}`,
31
+ '--build-arg',
32
+ `HOST_GID=${hostGid}`,
33
+ '--build-arg',
34
+ `AI_TOOL_PACKAGES=${toolNpmPackagesArg(tools)}`,
35
+ '--build-arg',
36
+ `AI_TOOLS_SHELL_INSTALL_B64=${toolShellInstallScriptBase64(tools)}`,
37
+ '--label',
38
+ sandboxLabel(config),
39
+ '--label',
40
+ `${sandboxImageConfigLabel(config)}=${imageSignature}`
41
+ ];
42
+ if (lastRefresh !== undefined) {
43
+ args.push('--label', `${sandboxImageRefreshLabel(config)}=${lastRefresh}`);
44
+ }
45
+ args.push('-f', toEnginePath(selectedEngine, dockerfilePath), toEnginePath(selectedEngine, config.repoRoot));
46
+ if (refresh) {
47
+ args.splice(1, 0, '--no-cache', '--pull');
48
+ }
49
+ return args;
50
+ }
51
+ export function parseRefreshTimestamp(value) {
52
+ const parsed = Number(value);
53
+ return Number.isInteger(parsed) && parsed >= 0 ? parsed : 0;
54
+ }
55
+ export function parseImageLabels(raw) {
56
+ try {
57
+ const parsed = JSON.parse(raw);
58
+ if (!parsed || typeof parsed !== 'object') {
59
+ return {};
60
+ }
61
+ return Object.fromEntries(Object.entries(parsed).filter((entry) => typeof entry[1] === 'string'));
62
+ }
63
+ catch {
64
+ return {};
65
+ }
66
+ }
67
+ export function isRefreshDisabled(env, noRefreshFlag) {
68
+ if (noRefreshFlag) {
69
+ return true;
70
+ }
71
+ const value = env.AI_SANDBOX_NO_REFRESH?.trim().toLowerCase();
72
+ return value === '1' || value === 'true' || value === 'yes';
73
+ }
74
+ export function isRefreshDue(lastRefresh, now, intervalDays) {
75
+ if (intervalDays <= 0 || lastRefresh > now) {
76
+ return false;
77
+ }
78
+ return now - lastRefresh >= intervalDays * 24 * 60 * 60 * 1000;
79
+ }
80
+ //# sourceMappingURL=image-build.js.map
@@ -1,6 +1,6 @@
1
1
  import * as p from '@clack/prompts';
2
- import { sandboxLabel } from "./constants.js";
3
- import { runEngine } from "./shell.js";
2
+ import { sandboxLabel } from './constants.js';
3
+ import { runEngine } from './shell.js';
4
4
  export function pruneSandboxDanglingImages(config, engine) {
5
5
  try {
6
6
  runEngine(engine, 'docker', [
@@ -37,12 +37,12 @@ export async function runSandbox(args) {
37
37
  }
38
38
  switch (subcommand) {
39
39
  case 'create': {
40
- const { create } = await import("./commands/create.js");
40
+ const { create } = await import('./commands/create.js');
41
41
  await create(rest);
42
42
  break;
43
43
  }
44
44
  case 'exec': {
45
- const { enter } = await import("./commands/enter.js");
45
+ const { enter } = await import('./commands/enter.js');
46
46
  const exitCode = await enter(rest);
47
47
  if (typeof exitCode === 'number' && exitCode !== 0) {
48
48
  process.exitCode = exitCode;
@@ -50,27 +50,27 @@ export async function runSandbox(args) {
50
50
  break;
51
51
  }
52
52
  case 'ls': {
53
- const { ls } = await import("./commands/ls.js");
53
+ const { ls } = await import('./commands/ls.js');
54
54
  ls(rest);
55
55
  break;
56
56
  }
57
57
  case 'show': {
58
- const { show } = await import("./commands/show.js");
58
+ const { show } = await import('./commands/show.js');
59
59
  show(rest);
60
60
  break;
61
61
  }
62
62
  case 'prune': {
63
- const { prune } = await import("./commands/prune.js");
63
+ const { prune } = await import('./commands/prune.js');
64
64
  await prune(rest);
65
65
  break;
66
66
  }
67
67
  case 'rebuild': {
68
- const { rebuild } = await import("./commands/rebuild.js");
68
+ const { rebuild } = await import('./commands/rebuild.js');
69
69
  await rebuild(rest);
70
70
  break;
71
71
  }
72
72
  case 'refresh': {
73
- const { refresh } = await import("./commands/refresh.js");
73
+ const { refresh } = await import('./commands/refresh.js');
74
74
  const exitCode = await refresh(rest);
75
75
  if (typeof exitCode === 'number' && exitCode !== 0) {
76
76
  process.exitCode = exitCode;
@@ -78,17 +78,17 @@ export async function runSandbox(args) {
78
78
  break;
79
79
  }
80
80
  case 'rm': {
81
- const { rm } = await import("./commands/rm.js");
81
+ const { rm } = await import('./commands/rm.js');
82
82
  await rm(rest);
83
83
  break;
84
84
  }
85
85
  case 'start': {
86
- const { start } = await import("./commands/start.js");
86
+ const { start } = await import('./commands/start.js');
87
87
  await start(rest);
88
88
  break;
89
89
  }
90
90
  case 'vm': {
91
- const { vm } = await import("./commands/vm.js");
91
+ const { vm } = await import('./commands/vm.js');
92
92
  await vm(rest);
93
93
  break;
94
94
  }
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { run } from "./shell.js";
3
+ import { run } from './shell.js';
4
4
  export function assertManagedPath(root, target) {
5
5
  const resolvedRoot = path.resolve(root);
6
6
  const resolvedTarget = path.resolve(target);
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { shareBranchDir, shareCommonDir } from "./constants.js";
3
+ import { shareBranchDir, shareCommonDir } from './constants.js';
4
4
  const DOTFILES_README = `# User-level dotfiles channel
5
5
 
6
6
  This directory is mounted **read-only** into every sandbox container at
@@ -1,5 +1,5 @@
1
- import { safeNameCandidates, sanitizeBranchName } from "./constants.js";
2
- import { hostJoin } from "./engines/wsl2-paths.js";
1
+ import { safeNameCandidates, sanitizeBranchName } from './constants.js';
2
+ import { hostJoin } from './engines/wsl2-paths.js';
3
3
  const TOOL_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
4
4
  function createBuiltinTools(home, project) {
5
5
  return {
@@ -1,4 +1,5 @@
1
- import { cardMessage, createFeishuTransport, normalizeMessage } from "./transport.js";
1
+ import { renderFeishuMessage } from './renderer.js';
2
+ import { createFeishuTransport, normalizeMessage } from './transport.js';
2
3
  // Assemble the feishu adapter. The transport is injectable so unit tests can
3
4
  // drive start/dispatch/reply/sendMessage against a fake transport without the
4
5
  // real SDK; production uses the default SDK-backed transport.
@@ -19,7 +20,10 @@ export function createFeishuAdapter(config, transport = createFeishuTransport(co
19
20
  messageId: normalized.messageId,
20
21
  raw: normalized.raw,
21
22
  reply: async (text) => {
22
- await transport.send(normalized.chatId, cardMessage(text));
23
+ await transport.send(normalized.chatId, renderFeishuMessage(text));
24
+ },
25
+ replyDisplay: async (message) => {
26
+ await transport.send(normalized.chatId, renderFeishuMessage(message));
23
27
  }
24
28
  };
25
29
  await adapterCtx.dispatch(message);
@@ -33,7 +37,10 @@ export function createFeishuAdapter(config, transport = createFeishuTransport(co
33
37
  await transport.stop();
34
38
  },
35
39
  async sendMessage(target, text) {
36
- await transport.send(target.chatId, cardMessage(text));
40
+ await transport.send(target.chatId, renderFeishuMessage(text));
41
+ },
42
+ async sendDisplayMessage(target, message) {
43
+ await transport.send(target.chatId, renderFeishuMessage(message));
37
44
  }
38
45
  };
39
46
  }
@@ -0,0 +1,88 @@
1
+ import { stripVTControlCharacters } from 'node:util';
2
+ import { normalizeOutbound, outboundToText } from '../../display.js';
3
+ const FEISHU_LARK_MD_MAX_LENGTH = 20_000;
4
+ function templateForTone(tone) {
5
+ if (tone === 'success')
6
+ return 'green';
7
+ if (tone === 'warning')
8
+ return 'yellow';
9
+ if (tone === 'danger')
10
+ return 'red';
11
+ return 'blue';
12
+ }
13
+ function templateForResult(exitCode) {
14
+ return exitCode === 0 ? 'green' : 'red';
15
+ }
16
+ export function cleanFeishuText(text) {
17
+ return stripVTControlCharacters(text).replace(/\r\n/g, '\n');
18
+ }
19
+ function clampLarkMarkdown(text) {
20
+ const clean = cleanFeishuText(text);
21
+ if (clean.length <= FEISHU_LARK_MD_MAX_LENGTH)
22
+ return clean;
23
+ return `${clean.slice(0, FEISHU_LARK_MD_MAX_LENGTH - 18)}\n\n...(truncated)`;
24
+ }
25
+ function div(content) {
26
+ return { tag: 'div', text: { tag: 'lark_md', content: clampLarkMarkdown(content) } };
27
+ }
28
+ function fields(rows) {
29
+ return {
30
+ tag: 'div',
31
+ fields: rows.map(([label, value]) => ({
32
+ is_short: true,
33
+ text: { tag: 'lark_md', content: clampLarkMarkdown(`**${label}**\n${value}`) }
34
+ }))
35
+ };
36
+ }
37
+ function card(title, template, elements) {
38
+ return {
39
+ msg_type: 'interactive',
40
+ content: JSON.stringify({
41
+ config: { wide_screen_mode: true },
42
+ header: { title: { tag: 'plain_text', content: cleanFeishuText(title) }, template },
43
+ elements
44
+ })
45
+ };
46
+ }
47
+ function tableMarkdown(message) {
48
+ return outboundToText(message);
49
+ }
50
+ function renderStatus(message) {
51
+ const elements = [];
52
+ if (message.fields && message.fields.length > 0)
53
+ elements.push(fields(message.fields));
54
+ if (message.body)
55
+ elements.push(div(message.body));
56
+ if (elements.length === 0)
57
+ elements.push(div(message.title));
58
+ return card(message.title, templateForTone(message.tone), elements);
59
+ }
60
+ function renderStream(message) {
61
+ const template = message.phase === 'finished' ? templateForResult(message.exitCode ?? null) : 'blue';
62
+ return card(message.title, template, [div(outboundToText(message))]);
63
+ }
64
+ function renderCommandResult(message) {
65
+ return card(message.title, templateForResult(message.exitCode), [div(outboundToText(message))]);
66
+ }
67
+ function renderUnknownDisplayKind(message) {
68
+ return card('agent-infra', 'blue', [div(`[unknown display kind: ${String(message.kind)}]`)]);
69
+ }
70
+ export function renderFeishuMessage(message) {
71
+ const normalized = normalizeOutbound(message);
72
+ switch (normalized.kind) {
73
+ case 'text':
74
+ return card('agent-infra', 'blue', [div(normalized.text)]);
75
+ case 'markdown':
76
+ return card('agent-infra', 'blue', [div(normalized.markdown)]);
77
+ case 'table':
78
+ return card(normalized.title ?? 'agent-infra', 'blue', [div(tableMarkdown(normalized))]);
79
+ case 'status-card':
80
+ return renderStatus(normalized);
81
+ case 'stream-event':
82
+ return renderStream(normalized);
83
+ case 'command-result':
84
+ return renderCommandResult(normalized);
85
+ }
86
+ return renderUnknownDisplayKind(normalized);
87
+ }
88
+ //# sourceMappingURL=renderer.js.map
@@ -1,5 +1,4 @@
1
1
  import * as lark from '@larksuiteoapi/node-sdk';
2
- import { stripVTControlCharacters } from 'node:util';
3
2
  function asRecord(value) {
4
3
  return typeof value === 'object' && value !== null ? value : {};
5
4
  }
@@ -41,21 +40,11 @@ export function normalizeMessage(event) {
41
40
  function resolveDomain(value) {
42
41
  return value === 'lark' || value === 'Lark' ? lark.Domain.Lark : lark.Domain.Feishu;
43
42
  }
44
- export function cleanFeishuText(text) {
45
- return stripVTControlCharacters(text).replace(/\r\n/g, '\n');
46
- }
47
- export function cardMessage(text) {
48
- return { kind: 'interactive', title: 'agent-infra', text: cleanFeishuText(text) };
49
- }
50
43
  export function toFeishuCreateData(chatId, message) {
51
44
  return {
52
45
  receive_id: chatId,
53
- msg_type: 'interactive',
54
- content: JSON.stringify({
55
- config: { wide_screen_mode: true },
56
- header: { title: { tag: 'plain_text', content: message.title }, template: 'blue' },
57
- elements: [{ tag: 'div', text: { tag: 'lark_md', content: message.text } }]
58
- })
46
+ msg_type: message.msg_type,
47
+ content: message.content
59
48
  };
60
49
  }
61
50
  // Build the real SDK-backed transport from adapter config. appId/appSecret come
@@ -1,14 +1,69 @@
1
- import { VERSION } from "../version.js";
2
- import { loadServerConfig } from "./config.js";
3
- import { createLogger } from "./logger.js";
4
- import { loadAdapters, unloadAdapters } from "./plugin-loader.js";
5
- import { authorize } from "./auth.js";
6
- import { commandHelp, parseCommand } from "./protocol.js";
7
- import { runAi } from "./runner.js";
8
- import { streamCommand } from "./streamer.js";
1
+ import { VERSION } from '../version.js';
2
+ import { loadServerConfig } from './config.js';
3
+ import { createLogger } from './logger.js';
4
+ import { loadAdapters, unloadAdapters } from './plugin-loader.js';
5
+ import { authorize } from './auth.js';
6
+ import { commandHelp, parseCommand } from './protocol.js';
7
+ import { runAi } from './runner.js';
8
+ import { streamCommand } from './streamer.js';
9
+ import { markdownMessage, replyOutbound, textMessage } from './display.js';
10
+ import { buildStatusModel, statusModelToDisplay } from '../task/commands/status.js';
9
11
  function errorMessage(error) {
10
12
  return error instanceof Error ? error.message : String(error);
11
13
  }
14
+ function isTaskStatus(argv) {
15
+ return argv[0] === 'task' && argv[1] === 'status' && typeof argv[2] === 'string';
16
+ }
17
+ export function createMessageDispatcher(options) {
18
+ const runAiImpl = options.runAi ?? runAi;
19
+ const buildStatusModelImpl = options.buildStatusModel ?? buildStatusModel;
20
+ const statusModelToDisplayImpl = options.statusModelToDisplay ?? statusModelToDisplay;
21
+ return async (message) => {
22
+ const plan = parseCommand(message.text);
23
+ if (plan.kind === 'ignore')
24
+ return;
25
+ if (plan.kind === 'error') {
26
+ await replyOutbound(message, textMessage(plan.message));
27
+ options.logger.info(`command rejected from ${message.adapter}:${message.userId}: ${plan.message}`);
28
+ return;
29
+ }
30
+ if (plan.kind === 'builtin' && plan.name === 'ping') {
31
+ await replyOutbound(message, textMessage(`pong ${VERSION}`));
32
+ return;
33
+ }
34
+ if (plan.kind === 'builtin' && plan.name === 'help') {
35
+ await replyOutbound(message, markdownMessage(commandHelp()));
36
+ return;
37
+ }
38
+ if (plan.kind === 'builtin' && plan.name === 'version') {
39
+ await replyOutbound(message, textMessage(`agent-infra ${VERSION}`));
40
+ return;
41
+ }
42
+ if (plan.kind === 'ai') {
43
+ const allowed = authorize({ adapter: message.adapter, userId: message.userId }, plan.role, options.config.auth);
44
+ if (!allowed.ok) {
45
+ await replyOutbound(message, textMessage(allowed.message));
46
+ options.logger.info(`unauthorized command from ${message.adapter}:${message.userId}: ${allowed.message}`);
47
+ return;
48
+ }
49
+ if (isTaskStatus(plan.argv)) {
50
+ try {
51
+ await replyOutbound(message, statusModelToDisplayImpl(buildStatusModelImpl(plan.argv[2])));
52
+ return;
53
+ }
54
+ catch {
55
+ // Fall back to the existing CLI streaming path. This preserves the
56
+ // old behavior for invalid refs and any status-model collection error.
57
+ }
58
+ }
59
+ await streamCommand({
60
+ title: `ai ${plan.argv.join(' ')}`,
61
+ chunkChars: typeof options.config.stream?.chunkChars === 'number' ? options.config.stream.chunkChars : 4000,
62
+ throttleMs: typeof options.config.stream?.throttleMs === 'number' ? options.config.stream.throttleMs : 1500
63
+ }, (emit) => runAiImpl(plan.argv, { onChunk: emit }), (outbound) => replyOutbound(message, outbound));
64
+ }
65
+ };
66
+ }
12
67
  // The daemon main loop. Runs in the detached child spawned by
13
68
  // process-control.start(), or in the foreground for debugging.
14
69
  //
@@ -35,41 +90,7 @@ export async function runDaemon() {
35
90
  const shutdown = new Promise((resolve) => {
36
91
  resolveShutdown = resolve;
37
92
  });
38
- const dispatch = async (message) => {
39
- const plan = parseCommand(message.text);
40
- if (plan.kind === 'ignore')
41
- return;
42
- if (plan.kind === 'error') {
43
- await message.reply(plan.message);
44
- logger.info(`command rejected from ${message.adapter}:${message.userId}: ${plan.message}`);
45
- return;
46
- }
47
- if (plan.kind === 'builtin' && plan.name === 'ping') {
48
- await message.reply(`pong ${VERSION}`);
49
- return;
50
- }
51
- if (plan.kind === 'builtin' && plan.name === 'help') {
52
- await message.reply(commandHelp());
53
- return;
54
- }
55
- if (plan.kind === 'builtin' && plan.name === 'version') {
56
- await message.reply(`agent-infra ${VERSION}`);
57
- return;
58
- }
59
- if (plan.kind === 'ai') {
60
- const allowed = authorize({ adapter: message.adapter, userId: message.userId }, plan.role, config.auth);
61
- if (!allowed.ok) {
62
- await message.reply(allowed.message);
63
- logger.info(`unauthorized command from ${message.adapter}:${message.userId}: ${allowed.message}`);
64
- return;
65
- }
66
- await streamCommand({
67
- title: `ai ${plan.argv.join(' ')}`,
68
- chunkChars: typeof config.stream?.chunkChars === 'number' ? config.stream.chunkChars : 4000,
69
- throttleMs: typeof config.stream?.throttleMs === 'number' ? config.stream.throttleMs : 1500
70
- }, (emit) => runAi(plan.argv, { onChunk: emit }), (text) => message.reply(text));
71
- }
72
- };
93
+ const dispatch = createMessageDispatcher({ config, logger });
73
94
  const ctx = { config, logger, dispatch, signal: abortController.signal };
74
95
  const adapters = await loadAdapters(config, ctx);
75
96
  logger.ok(`loaded ${adapters.length} adapter(s)`);
@@ -0,0 +1,83 @@
1
+ export function textMessage(text) {
2
+ return { kind: 'text', text };
3
+ }
4
+ export function markdownMessage(markdown) {
5
+ return { kind: 'markdown', markdown };
6
+ }
7
+ export function tableMessage(columns, rows, title) {
8
+ return { kind: 'table', title, columns, rows };
9
+ }
10
+ export function statusCard(title, tone, fields, body) {
11
+ return { kind: 'status-card', title, tone, fields, body };
12
+ }
13
+ export function streamEvent(title, phase, text, exitCode, signal) {
14
+ const message = { kind: 'stream-event', title, phase };
15
+ if (text !== undefined)
16
+ message.text = text;
17
+ if (exitCode !== undefined)
18
+ message.exitCode = exitCode;
19
+ if (signal !== undefined)
20
+ message.signal = signal;
21
+ return message;
22
+ }
23
+ export function commandResult(title, exitCode, signal, stdout, stderr) {
24
+ return { kind: 'command-result', title, exitCode, signal, stdout, stderr };
25
+ }
26
+ export function normalizeOutbound(message) {
27
+ return typeof message === 'string' ? textMessage(message) : message;
28
+ }
29
+ function tableToText(message) {
30
+ const lines = [];
31
+ if (message.title)
32
+ lines.push(message.title);
33
+ lines.push(message.columns.join(' | '));
34
+ lines.push(message.columns.map(() => '---').join(' | '));
35
+ for (const row of message.rows) {
36
+ lines.push(row.join(' | '));
37
+ }
38
+ return lines.join('\n');
39
+ }
40
+ function compact(lines) {
41
+ return lines.filter((line) => typeof line === 'string' && line !== '').join('\n');
42
+ }
43
+ function unknownDisplayKind(message) {
44
+ return `[unknown: ${String(message.kind)}]`;
45
+ }
46
+ export function outboundToText(message) {
47
+ const normalized = normalizeOutbound(message);
48
+ switch (normalized.kind) {
49
+ case 'text':
50
+ return normalized.text;
51
+ case 'markdown':
52
+ return normalized.markdown;
53
+ case 'table':
54
+ return tableToText(normalized);
55
+ case 'status-card':
56
+ return compact([
57
+ normalized.title,
58
+ ...(normalized.fields ?? []).map(([label, value]) => `${label}: ${value}`),
59
+ normalized.body
60
+ ]);
61
+ case 'stream-event':
62
+ if (normalized.phase === 'started')
63
+ return `started ${normalized.title}`;
64
+ if (normalized.phase === 'chunk')
65
+ return normalized.text ?? '';
66
+ return `finished ${normalized.title} exitCode=${normalized.exitCode ?? 'null'} signal=${normalized.signal ?? 'null'}`;
67
+ case 'command-result':
68
+ return compact([
69
+ `finished ${normalized.title} exitCode=${normalized.exitCode ?? 'null'} signal=${normalized.signal ?? 'null'}`,
70
+ normalized.stdout,
71
+ normalized.stderr
72
+ ]);
73
+ }
74
+ return unknownDisplayKind(normalized);
75
+ }
76
+ export async function replyOutbound(inbound, message) {
77
+ if (inbound.replyDisplay) {
78
+ await inbound.replyDisplay(message);
79
+ return;
80
+ }
81
+ await inbound.reply(outboundToText(message));
82
+ }
83
+ //# sourceMappingURL=display.js.map
@@ -1,5 +1,5 @@
1
- import { runDaemon } from "./daemon.js";
2
- import { start, stop, status, logs } from "./process-control.js";
1
+ import { runDaemon } from './daemon.js';
2
+ import { start, stop, status, logs } from './process-control.js';
3
3
  const USAGE = `Usage: ai server <command> [options]
4
4
 
5
5
  Commands:
@@ -1,8 +1,8 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { spawn, execFileSync } from 'node:child_process';
4
- import { loadServerConfig } from "./config.js";
5
- import { runDaemon } from "./daemon.js";
4
+ import { loadServerConfig } from './config.js';
5
+ import { runDaemon } from './daemon.js';
6
6
  export function buildStopCommand(pid, platform) {
7
7
  if (platform === 'win32') {
8
8
  return { kind: 'exec', command: 'taskkill', args: ['/PID', String(pid), '/T', '/F'] };
@@ -1,5 +1,5 @@
1
- import { VERSION } from "../version.js";
2
- import { getSkillRunSpec } from "../run/skills.js";
1
+ import { VERSION } from '../version.js';
2
+ import { getSkillRunSpec } from '../run/skills.js';
3
3
  const TASK_READ = new Set(['ls', 'list', 'status', 'show', 'log', 'decisions']);
4
4
  const SANDBOX_READ = new Set(['ls', 'list', 'show']);
5
5
  const SANDBOX_WRITE = new Set(['create', 'start']);
@@ -1,4 +1,5 @@
1
- import { redactSecrets } from "./redact.js";
1
+ import { streamEvent } from './display.js';
2
+ import { redactSecrets } from './redact.js';
2
3
  function chunks(text, size) {
3
4
  if (text.length === 0)
4
5
  return [];
@@ -9,7 +10,7 @@ function chunks(text, size) {
9
10
  return out;
10
11
  }
11
12
  export async function streamCommand(options, run, send) {
12
- await send(`started ${options.title}`);
13
+ await send(streamEvent(options.title, 'started'));
13
14
  const size = options.chunkChars ?? 4000;
14
15
  const throttleMs = options.throttleMs ?? 0;
15
16
  let streamed = false;
@@ -22,7 +23,7 @@ export async function streamCommand(options, run, send) {
22
23
  buffer = '';
23
24
  lastFlush = Date.now();
24
25
  for (const chunk of chunks(text, size)) {
25
- await send(chunk);
26
+ await send(streamEvent(options.title, 'chunk', chunk));
26
27
  }
27
28
  };
28
29
  const emit = async (chunk) => {
@@ -38,7 +39,7 @@ export async function streamCommand(options, run, send) {
38
39
  buffer += [result.stdout, result.stderr].filter(Boolean).join('\n');
39
40
  }
40
41
  await flush();
41
- await send(`finished ${options.title} exitCode=${result.exitCode} signal=${result.signal ?? 'null'}`);
42
+ await send(streamEvent(options.title, 'finished', undefined, result.exitCode, result.signal));
42
43
  return result;
43
44
  }
44
45
  //# sourceMappingURL=streamer.js.map
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs';
2
- import { resolveTaskRef } from "../resolve-ref.js";
3
- import { resolveArtifact } from "../artifacts.js";
2
+ import { resolveTaskRef } from '../resolve-ref.js';
3
+ import { resolveArtifact } from '../artifacts.js';
4
4
  const USAGE = `Usage: ai task cat <N | #N | TASK-id> <artifact | N>
5
5
 
6
6
  Prints a task artifact's raw content to stdout.
@@ -1,9 +1,9 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { formatTable } from "../../table.js";
4
- import { resolveTaskRef } from "../resolve-ref.js";
5
- import { parseLedger, HUMAN_DECISION_STATUSES } from "../ledger.js";
6
- import { extractSubSection } from "../sections.js";
3
+ import { formatTable } from '../../table.js';
4
+ import { resolveTaskRef } from '../resolve-ref.js';
5
+ import { parseLedger, HUMAN_DECISION_STATUSES } from '../ledger.js';
6
+ import { extractSubSection } from '../sections.js';
7
7
  const USAGE = `Usage: ai task decisions <N | #N | TASK-id> [selector] [options]
8
8
 
9
9
  Lists the human-decision (HD-) items recorded in a task's review disagreement
@@ -1,6 +1,6 @@
1
- import { formatTable } from "../../table.js";
2
- import { resolveTaskRef } from "../resolve-ref.js";
3
- import { enumerateArtifacts } from "../artifacts.js";
1
+ import { formatTable } from '../../table.js';
2
+ import { resolveTaskRef } from '../resolve-ref.js';
3
+ import { enumerateArtifacts } from '../artifacts.js';
4
4
  const USAGE = `Usage: ai task files <N | #N | TASK-id>
5
5
 
6
6
  Lists the artifacts in a task directory with stable numbers.
@@ -1,8 +1,8 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { resolveTaskRef, detectRepoRoot, enumerateTaskDirs } from "../resolve-ref.js";
4
- import { enumerateArtifacts, resolveArtifact } from "../artifacts.js";
5
- import { loadShortIdByTaskId } from "../short-id.js";
3
+ import { resolveTaskRef, detectRepoRoot, enumerateTaskDirs } from '../resolve-ref.js';
4
+ import { enumerateArtifacts, resolveArtifact } from '../artifacts.js';
5
+ import { loadShortIdByTaskId } from '../short-id.js';
6
6
  const USAGE = `Usage: ai task grep <pattern> [ref] [artifact | N]
7
7
 
8
8
  Literal (non-regex) line search across task artifacts.