@agent-spaces/server 0.2.9 → 0.3.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 (185) hide show
  1. package/dist/adapters/claude-code-runtime/index.js +48 -6
  2. package/dist/adapters/claude-code-runtime/message-format.js +3 -3
  3. package/dist/adapters/git.js +140 -2
  4. package/dist/agents/agent-designer.js +334 -0
  5. package/dist/agents/agent-message-parts.js +8 -0
  6. package/dist/agents/commit-agent.js +9 -2
  7. package/dist/agents/issue-task-controller.js +14 -4
  8. package/dist/app.js +53 -1
  9. package/dist/package.json +1 -1
  10. package/dist/routes/agent-sse.js +4 -1
  11. package/dist/routes/agent.js +15 -0
  12. package/dist/routes/channel.js +1 -1
  13. package/dist/routes/file.js +97 -1
  14. package/dist/routes/git.js +122 -1
  15. package/dist/routes/iframe-bookmarks.js +33 -0
  16. package/dist/routes/speech-recognition.js +80 -0
  17. package/dist/services/agent.js +48 -5
  18. package/dist/services/builtin-tools.js +67 -1
  19. package/dist/services/channel.js +2 -0
  20. package/dist/services/command-process-manager.js +14 -1
  21. package/dist/services/file.js +86 -6
  22. package/dist/services/gitignore.js +65 -0
  23. package/dist/services/issue-retry.js +2 -1
  24. package/dist/services/issue.js +2 -2
  25. package/dist/services/notification-hub/bot-agent.js +9 -1
  26. package/dist/services/persistent-agent-context.js +155 -0
  27. package/dist/services/pty.js +47 -2
  28. package/dist/services/search.js +11 -3
  29. package/dist/services/speech-recognition/base.js +3 -0
  30. package/dist/services/speech-recognition/index.js +16 -0
  31. package/dist/services/speech-recognition/tencent.js +112 -0
  32. package/dist/storage/iframe-bookmarks-store.js +46 -0
  33. package/dist/storage/speech-recognition-store.js +58 -0
  34. package/dist/web/404.html +1 -1
  35. package/dist/web/__next.__PAGE__.txt +4 -4
  36. package/dist/web/__next._full.txt +31 -26
  37. package/dist/web/__next._head.txt +4 -4
  38. package/dist/web/__next._index.txt +17 -13
  39. package/dist/web/__next._tree.txt +2 -2
  40. package/dist/web/_next/static/chunks/0-mtp2au0a0nh.js +189 -0
  41. package/dist/web/_next/static/chunks/00amm3dvu3po9.js +31 -0
  42. package/dist/web/_next/static/chunks/02unyp1obddyn.js +3 -0
  43. package/dist/web/_next/static/chunks/{0-vgd3j-zh3m8.js → 05iy4.c-1cfhd.js} +1 -1
  44. package/dist/web/_next/static/chunks/0d.v1csiyl.1~.js +2 -0
  45. package/dist/web/_next/static/chunks/0egot8y~i-ge3.js +1 -0
  46. package/dist/web/_next/static/chunks/0e~0o6v_awcod.js +1 -0
  47. package/dist/web/_next/static/chunks/0fpf.rso9mhc0.js +1 -0
  48. package/dist/web/_next/static/chunks/0glol5jdarfma.js +2 -0
  49. package/dist/web/_next/static/chunks/0m1ud--um8kjh.js +1 -0
  50. package/dist/web/_next/static/chunks/0o6w-uw2p.phy.js +5 -0
  51. package/dist/web/_next/static/chunks/0os7q6w~-tmg7.css +1 -0
  52. package/dist/web/_next/static/chunks/0plreh4_gkxf9.js +1 -0
  53. package/dist/web/_next/static/chunks/0u7_qd9hzqwac.js +1 -0
  54. package/dist/web/_next/static/chunks/0vesz93a98px4.js +1 -0
  55. package/dist/web/_next/static/chunks/0w3o.7vi.dlm6.js +1 -0
  56. package/dist/web/_next/static/chunks/0xtmr0y4vo-~p.js +1 -0
  57. package/dist/web/_next/static/chunks/0~3uikgc074il.js +1 -0
  58. package/dist/web/_next/static/chunks/{0y5ipuqgqfrob.js → 0~xn0gfqxa20r.js} +3 -3
  59. package/dist/web/_next/static/chunks/11ozf700niy2z.js +1 -0
  60. package/dist/web/_next/static/chunks/{0jmsjlawtg6oa.js → 145gbwitjxamg.js} +1 -1
  61. package/dist/web/_next/static/chunks/{turbopack-0lxiiw.jhevml.js → turbopack-0kcwat8.leugk.js} +1 -1
  62. package/dist/web/_not-found/__next._full.txt +34 -30
  63. package/dist/web/_not-found/__next._head.txt +4 -4
  64. package/dist/web/_not-found/__next._index.txt +17 -13
  65. package/dist/web/_not-found/__next._not-found.__PAGE__.txt +2 -2
  66. package/dist/web/_not-found/__next._not-found.txt +3 -3
  67. package/dist/web/_not-found/__next._tree.txt +2 -2
  68. package/dist/web/_not-found.html +1 -1
  69. package/dist/web/_not-found.txt +34 -30
  70. package/dist/web/index.html +1 -1
  71. package/dist/web/index.txt +31 -26
  72. package/dist/web/login/__next._full.txt +35 -29
  73. package/dist/web/login/__next._head.txt +4 -4
  74. package/dist/web/login/__next._index.txt +17 -13
  75. package/dist/web/login/__next._tree.txt +2 -2
  76. package/dist/web/login/__next.login.__PAGE__.txt +4 -4
  77. package/dist/web/login/__next.login.txt +3 -3
  78. package/dist/web/login.html +1 -1
  79. package/dist/web/login.txt +35 -29
  80. package/dist/web/settings/__next._full.txt +36 -34
  81. package/dist/web/settings/__next._head.txt +4 -4
  82. package/dist/web/settings/__next._index.txt +17 -13
  83. package/dist/web/settings/__next._tree.txt +2 -2
  84. package/dist/web/settings/__next.settings.__PAGE__.txt +4 -4
  85. package/dist/web/settings/__next.settings.txt +5 -5
  86. package/dist/web/settings/agents/__next._full.txt +38 -35
  87. package/dist/web/settings/agents/__next._head.txt +4 -4
  88. package/dist/web/settings/agents/__next._index.txt +17 -13
  89. package/dist/web/settings/agents/__next._tree.txt +2 -2
  90. package/dist/web/settings/agents/__next.settings.agents.__PAGE__.txt +4 -4
  91. package/dist/web/settings/agents/__next.settings.agents.txt +3 -3
  92. package/dist/web/settings/agents/__next.settings.txt +5 -5
  93. package/dist/web/settings/agents.html +1 -1
  94. package/dist/web/settings/agents.txt +38 -35
  95. package/dist/web/settings/mcps/__next._full.txt +38 -35
  96. package/dist/web/settings/mcps/__next._head.txt +4 -4
  97. package/dist/web/settings/mcps/__next._index.txt +17 -13
  98. package/dist/web/settings/mcps/__next._tree.txt +2 -2
  99. package/dist/web/settings/mcps/__next.settings.mcps.__PAGE__.txt +4 -4
  100. package/dist/web/settings/mcps/__next.settings.mcps.txt +3 -3
  101. package/dist/web/settings/mcps/__next.settings.txt +5 -5
  102. package/dist/web/settings/mcps.html +1 -1
  103. package/dist/web/settings/mcps.txt +38 -35
  104. package/dist/web/settings/models/__next._full.txt +38 -35
  105. package/dist/web/settings/models/__next._head.txt +4 -4
  106. package/dist/web/settings/models/__next._index.txt +17 -13
  107. package/dist/web/settings/models/__next._tree.txt +2 -2
  108. package/dist/web/settings/models/__next.settings.models.__PAGE__.txt +4 -4
  109. package/dist/web/settings/models/__next.settings.models.txt +3 -3
  110. package/dist/web/settings/models/__next.settings.txt +5 -5
  111. package/dist/web/settings/models.html +1 -1
  112. package/dist/web/settings/models.txt +38 -35
  113. package/dist/web/settings/providers/__next._full.txt +38 -35
  114. package/dist/web/settings/providers/__next._head.txt +4 -4
  115. package/dist/web/settings/providers/__next._index.txt +17 -13
  116. package/dist/web/settings/providers/__next._tree.txt +2 -2
  117. package/dist/web/settings/providers/__next.settings.providers.__PAGE__.txt +4 -4
  118. package/dist/web/settings/providers/__next.settings.providers.txt +3 -3
  119. package/dist/web/settings/providers/__next.settings.txt +5 -5
  120. package/dist/web/settings/providers.html +1 -1
  121. package/dist/web/settings/providers.txt +38 -35
  122. package/dist/web/settings/skills/__next._full.txt +38 -35
  123. package/dist/web/settings/skills/__next._head.txt +4 -4
  124. package/dist/web/settings/skills/__next._index.txt +17 -13
  125. package/dist/web/settings/skills/__next._tree.txt +2 -2
  126. package/dist/web/settings/skills/__next.settings.skills.__PAGE__.txt +4 -4
  127. package/dist/web/settings/skills/__next.settings.skills.txt +3 -3
  128. package/dist/web/settings/skills/__next.settings.txt +5 -5
  129. package/dist/web/settings/skills.html +1 -1
  130. package/dist/web/settings/skills.txt +38 -35
  131. package/dist/web/settings.html +1 -1
  132. package/dist/web/settings.txt +36 -34
  133. package/dist/web/workflows/__next._full.txt +36 -31
  134. package/dist/web/workflows/__next._head.txt +4 -4
  135. package/dist/web/workflows/__next._index.txt +17 -13
  136. package/dist/web/workflows/__next._tree.txt +2 -2
  137. package/dist/web/workflows/__next.workflows.__PAGE__.txt +4 -4
  138. package/dist/web/workflows/__next.workflows.txt +3 -3
  139. package/dist/web/workflows.html +1 -1
  140. package/dist/web/workflows.txt +36 -31
  141. package/dist/web/workspace/_/__next._full.txt +39 -34
  142. package/dist/web/workspace/_/__next._head.txt +4 -4
  143. package/dist/web/workspace/_/__next._index.txt +17 -13
  144. package/dist/web/workspace/_/__next._tree.txt +2 -2
  145. package/dist/web/workspace/_/__next.workspace.$d$id.__PAGE__.txt +3 -3
  146. package/dist/web/workspace/_/__next.workspace.$d$id.txt +3 -3
  147. package/dist/web/workspace/_/__next.workspace.txt +3 -3
  148. package/dist/web/workspace/_.html +1 -1
  149. package/dist/web/workspace/_.txt +39 -34
  150. package/dist/web/workspaces/__next._full.txt +35 -30
  151. package/dist/web/workspaces/__next._head.txt +4 -4
  152. package/dist/web/workspaces/__next._index.txt +17 -13
  153. package/dist/web/workspaces/__next._tree.txt +2 -2
  154. package/dist/web/workspaces/__next.workspaces.__PAGE__.txt +4 -4
  155. package/dist/web/workspaces/__next.workspaces.txt +3 -3
  156. package/dist/web/workspaces.html +1 -1
  157. package/dist/web/workspaces.txt +35 -30
  158. package/dist/ws/agent-prompt.js +48 -18
  159. package/dist/ws/agent-runner.js +4 -1
  160. package/dist/ws/message-parts.js +8 -0
  161. package/dist/ws/terminal-handler.js +1 -1
  162. package/package.json +1 -1
  163. package/public/avatars/1778953228323-5je54x-user.jpg +0 -0
  164. package/public/avatars/1778954057880-g01q6y-user.jpg +0 -0
  165. package/dist/web/_next/static/chunks/0.~n0s82y4zh_.css +0 -1
  166. package/dist/web/_next/static/chunks/002-dxhags4dj.js +0 -2
  167. package/dist/web/_next/static/chunks/02duknlg3rgjp.js +0 -1
  168. package/dist/web/_next/static/chunks/02wkg1tjv~_t2.js +0 -1
  169. package/dist/web/_next/static/chunks/03jy~o.gx._e-.js +0 -3
  170. package/dist/web/_next/static/chunks/04zxh2e-4~-xs.js +0 -1
  171. package/dist/web/_next/static/chunks/07bs5ds68tg38.js +0 -2
  172. package/dist/web/_next/static/chunks/082lp-xqv7r.-.js +0 -183
  173. package/dist/web/_next/static/chunks/0b2tump5duj9j.js +0 -1
  174. package/dist/web/_next/static/chunks/0csqa76ud3h4d.js +0 -1
  175. package/dist/web/_next/static/chunks/0n0j7tw_y1qk2.js +0 -1
  176. package/dist/web/_next/static/chunks/0pep4mkvt3.rh.js +0 -31
  177. package/dist/web/_next/static/chunks/0tkii3.lkbu3u.js +0 -1
  178. package/dist/web/_next/static/chunks/0wz8~mvztv-vq.js +0 -1
  179. package/dist/web/_next/static/chunks/0zl19l5tuoppw.js +0 -1
  180. package/dist/web/_next/static/chunks/0zxtxecpqyoeh.js +0 -1
  181. package/dist/web/_next/static/chunks/12sucjx_z8ftc.js +0 -1
  182. package/dist/web/_next/static/chunks/15_n_khh8cuax.js +0 -5
  183. /package/dist/web/_next/static/{hGfVPALj5nbwettayL9DY → 4dBAEZ275uRqcyeaRmXSx}/_buildManifest.js +0 -0
  184. /package/dist/web/_next/static/{hGfVPALj5nbwettayL9DY → 4dBAEZ275uRqcyeaRmXSx}/_clientMiddlewareManifest.js +0 -0
  185. /package/dist/web/_next/static/{hGfVPALj5nbwettayL9DY → 4dBAEZ275uRqcyeaRmXSx}/_ssgManifest.js +0 -0
@@ -6,6 +6,17 @@ import * as taskService from './task.js';
6
6
  import * as agentService from './agent.js';
7
7
  import * as commandService from './command.js';
8
8
  import * as commandProcessManager from './command-process-manager.js';
9
+ const issueStatuses = [
10
+ 'draft',
11
+ 'planned',
12
+ 'in_progress',
13
+ 'review_pending',
14
+ 'changes_requested',
15
+ 'approved',
16
+ 'completed',
17
+ 'archived',
18
+ 'error',
19
+ ];
9
20
  const currentChannelInputSchema = {
10
21
  type: 'object',
11
22
  properties: {
@@ -32,6 +43,11 @@ const createIssueInputSchema = {
32
43
  type: 'string',
33
44
  description: 'Issue description to create for the current channel.',
34
45
  },
46
+ status: {
47
+ type: 'string',
48
+ enum: issueStatuses,
49
+ description: 'Issue status to create. Use completed when creating a finished issue report. Defaults to draft.',
50
+ },
35
51
  },
36
52
  required: ['channelId', 'title'],
37
53
  additionalProperties: false,
@@ -51,6 +67,32 @@ const addCommentInputSchema = {
51
67
  required: ['channelId', 'content'],
52
68
  additionalProperties: false,
53
69
  };
70
+ const readTerminalOutputInputSchema = {
71
+ type: 'object',
72
+ properties: {
73
+ workspaceId: {
74
+ type: 'string',
75
+ description: 'Optional workspace ID. If omitted, the current workspace is used.',
76
+ },
77
+ sessionId: {
78
+ type: 'string',
79
+ description: 'Terminal session ID to read.',
80
+ },
81
+ offset: {
82
+ type: 'integer',
83
+ minimum: 0,
84
+ description: 'Number of newest lines to skip before reading. Defaults to 0.',
85
+ },
86
+ limit: {
87
+ type: 'integer',
88
+ minimum: 1,
89
+ maximum: 1000,
90
+ description: 'Maximum number of lines to read. Defaults to 100.',
91
+ },
92
+ },
93
+ required: ['sessionId'],
94
+ additionalProperties: false,
95
+ };
54
96
  export function createIssueFunctionTools(workspaceId, channel, actor, allowedTools) {
55
97
  if (!channel)
56
98
  return [];
@@ -94,9 +136,10 @@ function createCurrentChannelIssue(workspaceId, channel, input) {
94
136
  throw new Error(`Current channel already has a bound issue: ${currentChannel.issueId}`);
95
137
  const title = typeof data.title === 'string' ? data.title.trim() : '';
96
138
  const description = typeof data.description === 'string' ? data.description.trim() : '';
139
+ const status = parseIssueStatus(data.status);
97
140
  if (!title)
98
141
  throw new Error('title is required.');
99
- const issue = issueService.createForChannel(workspaceId, currentChannel.id, { title, description });
142
+ const issue = issueService.createForChannel(workspaceId, currentChannel.id, { title, description, status });
100
143
  if (!issue)
101
144
  throw new Error(`Current channel not found: ${currentChannel.id}`);
102
145
  channel.issueId = issue.id;
@@ -104,6 +147,14 @@ function createCurrentChannelIssue(workspaceId, channel, input) {
104
147
  channel.name = title;
105
148
  return issue;
106
149
  }
150
+ function parseIssueStatus(status) {
151
+ if (status === undefined)
152
+ return undefined;
153
+ if (typeof status !== 'string' || !issueStatuses.includes(status)) {
154
+ throw new Error(`status must be one of: ${issueStatuses.join(', ')}.`);
155
+ }
156
+ return status;
157
+ }
107
158
  function viewCurrentChannelIssue(workspaceId, channel, input) {
108
159
  assertCurrentChannelId(channel, input);
109
160
  const issue = getBoundIssue(workspaceId, channel);
@@ -185,6 +236,21 @@ function assertCurrentChannelId(channel, input) {
185
236
  }
186
237
  export function createCommandFunctionTools(workspaceId) {
187
238
  return [
239
+ {
240
+ name: 'ReadTerminalOutput',
241
+ description: 'Read paginated terminal output by terminal session ID. Defaults to the newest 100 lines.',
242
+ inputSchema: readTerminalOutputInputSchema,
243
+ annotations: { readOnly: true, openWorld: false },
244
+ execute: async (input) => {
245
+ const data = input;
246
+ if (data.workspaceId && data.workspaceId !== workspaceId)
247
+ throw new Error('workspaceId mismatch');
248
+ return commandProcessManager.readTerminalOutput(workspaceId, data.sessionId, {
249
+ offset: data.offset,
250
+ limit: data.limit,
251
+ });
252
+ },
253
+ },
188
254
  {
189
255
  name: 'ListQuickCommands',
190
256
  description: 'List all quick commands for the workspace with running status.',
@@ -53,6 +53,8 @@ export function updateChannel(workspaceId, channelId, data) {
53
53
  channels[idx].draft = data.draft;
54
54
  if (Object.hasOwn(data, 'todos'))
55
55
  channels[idx].todos = data.todos;
56
+ if (Object.hasOwn(data, 'archived'))
57
+ channels[idx].archived = data.archived;
56
58
  writeJsonFile(channelsPath(workspaceId), channels);
57
59
  return channels[idx];
58
60
  }
@@ -1,3 +1,6 @@
1
+ // packages/server/src/services/command-process-manager.ts
2
+ import { statSync } from 'node:fs';
3
+ import { dirname } from 'node:path';
1
4
  import * as ptyService from './pty.js';
2
5
  import * as commandService from './command.js';
3
6
  import { getWorkspace } from '../storage/workspace-store.js';
@@ -15,7 +18,14 @@ export function runCommand(workspaceId, commandId) {
15
18
  if (!command)
16
19
  throw new Error('Command not found');
17
20
  const workspace = getWorkspace(workspaceId);
18
- const cwd = command.cwd || workspace?.boundDirs[0] || process.env.HOME || '/tmp';
21
+ const rawCwd = command.cwd || workspace?.boundDirs[0] || process.env.HOME || process.env.USERPROFILE || (process.platform === 'win32' ? process.env.SYSTEMROOT || 'C:\\' : '/tmp');
22
+ let cwd;
23
+ try {
24
+ cwd = statSync(rawCwd).isFile() ? dirname(rawCwd) : rawCwd;
25
+ }
26
+ catch {
27
+ cwd = rawCwd;
28
+ }
19
29
  const shell = command.shell;
20
30
  const env = command.env;
21
31
  console.log(`[command] runCommand: workspace=${workspaceId} command=${commandId} cwd=${cwd} shell=${shell} cmd=${command.command}`);
@@ -121,6 +131,9 @@ export function getCommandProcesses(workspaceId) {
121
131
  }
122
132
  return result;
123
133
  }
134
+ export function readTerminalOutput(workspaceId, sessionId, options) {
135
+ return ptyService.readSessionOutput(workspaceId, sessionId, options);
136
+ }
124
137
  export function cleanup(workspaceId) {
125
138
  for (const [commandId, cmdProcess] of processes) {
126
139
  if (cmdProcess.workspaceId === workspaceId) {
@@ -1,6 +1,7 @@
1
- import { readdir, stat, readFile, writeFile, mkdir, rm } from 'node:fs/promises';
2
- import { join, resolve, relative, isAbsolute } from 'node:path';
1
+ import { readdir, stat, readFile, writeFile, mkdir, rm, rename as fsRename } from 'node:fs/promises';
2
+ import { join, resolve, relative, isAbsolute, dirname } from 'node:path';
3
3
  import { getWorkspace } from '../storage/workspace-store.js';
4
+ import { createGitignoreFilter } from './gitignore.js';
4
5
  const IGNORED_DIRS = new Set(['.git', 'node_modules', '.next', '.DS_Store']);
5
6
  export function resolvePath(workspace, relPath) {
6
7
  const base = resolve(workspace.boundDirs[0]);
@@ -10,25 +11,33 @@ export function resolvePath(workspace, relPath) {
10
11
  return null;
11
12
  return abs;
12
13
  }
13
- export async function readTree(workspace, relPath = '') {
14
+ export async function readTree(workspace, relPath = '', depth = Infinity) {
14
15
  const dirPath = resolvePath(workspace, relPath);
15
16
  if (!dirPath)
16
17
  return [];
17
- const entries = await readdir(dirPath, { withFileTypes: true });
18
+ const baseDir = resolve(workspace.boundDirs[0]);
19
+ const ig = await createGitignoreFilter(baseDir);
20
+ try {
21
+ var entries = await readdir(dirPath, { withFileTypes: true });
22
+ }
23
+ catch {
24
+ return [];
25
+ }
18
26
  const nodes = [];
19
27
  for (const entry of entries) {
20
28
  if (IGNORED_DIRS.has(entry.name))
21
29
  continue;
22
30
  const fullPath = join(dirPath, entry.name);
23
31
  const entryRelPath = relPath ? `${relPath}/${entry.name}` : entry.name;
32
+ const isIgnored = ig.isIgnored(entryRelPath, entry.name, entry.isDirectory());
24
33
  const s = await stat(fullPath);
25
34
  if (entry.isDirectory()) {
26
- const children = await readTree(workspace, entryRelPath);
27
35
  nodes.push({
28
36
  name: entry.name,
29
37
  path: entryRelPath,
30
38
  type: 'directory',
31
- children,
39
+ ignored: isIgnored || undefined,
40
+ children: depth > 1 ? await readTree(workspace, entryRelPath, depth - 1) : undefined,
32
41
  });
33
42
  }
34
43
  else {
@@ -36,6 +45,7 @@ export async function readTree(workspace, relPath = '') {
36
45
  name: entry.name,
37
46
  path: entryRelPath,
38
47
  type: 'file',
48
+ ignored: isIgnored || undefined,
39
49
  size: s.size,
40
50
  modifiedAt: s.mtime.toISOString(),
41
51
  });
@@ -85,5 +95,75 @@ export async function deletePath(workspace, relPath) {
85
95
  return false;
86
96
  }
87
97
  }
98
+ export async function writeFileBinary(workspace, relPath, buffer) {
99
+ const abs = resolvePath(workspace, relPath);
100
+ if (!abs)
101
+ return false;
102
+ try {
103
+ await mkdir(join(abs, '..'), { recursive: true });
104
+ await writeFile(abs, buffer);
105
+ return true;
106
+ }
107
+ catch {
108
+ return false;
109
+ }
110
+ }
111
+ export async function importFromUrl(workspace, url, targetDir) {
112
+ try {
113
+ const res = await fetch(url);
114
+ if (!res.ok)
115
+ return null;
116
+ const buffer = Buffer.from(await res.arrayBuffer());
117
+ const urlPath = new URL(url).pathname;
118
+ const filename = decodeURIComponent(urlPath.split('/').pop() || 'downloaded-file');
119
+ const relPath = targetDir ? `${targetDir}/${filename}` : filename;
120
+ const ok = await writeFileBinary(workspace, relPath, buffer);
121
+ return ok ? relPath : null;
122
+ }
123
+ catch {
124
+ return null;
125
+ }
126
+ }
127
+ export async function importFromAbsPath(workspace, absPath, targetDir) {
128
+ try {
129
+ const content = await readFile(absPath);
130
+ const filename = absPath.split(/[/\\]/).pop() || 'imported-file';
131
+ const relPath = targetDir ? `${targetDir}/${filename}` : filename;
132
+ const ok = await writeFileBinary(workspace, relPath, content);
133
+ return ok ? relPath : null;
134
+ }
135
+ catch {
136
+ return null;
137
+ }
138
+ }
139
+ export async function renamePath(workspace, oldRelPath, newRelPath) {
140
+ const oldAbs = resolvePath(workspace, oldRelPath);
141
+ const newAbs = resolvePath(workspace, newRelPath);
142
+ if (!oldAbs || !newAbs)
143
+ return false;
144
+ try {
145
+ await mkdir(dirname(newAbs), { recursive: true });
146
+ await fsRename(oldAbs, newAbs);
147
+ return true;
148
+ }
149
+ catch {
150
+ return false;
151
+ }
152
+ }
153
+ export async function copyPath(workspace, srcRelPath, destRelPath) {
154
+ const srcAbs = resolvePath(workspace, srcRelPath);
155
+ const destAbs = resolvePath(workspace, destRelPath);
156
+ if (!srcAbs || !destAbs)
157
+ return false;
158
+ try {
159
+ const { cp } = await import('node:fs/promises');
160
+ await mkdir(dirname(destAbs), { recursive: true });
161
+ await cp(srcAbs, destAbs, { recursive: true });
162
+ return true;
163
+ }
164
+ catch {
165
+ return false;
166
+ }
167
+ }
88
168
  export { getWorkspace };
89
169
  //# sourceMappingURL=file.js.map
@@ -0,0 +1,65 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ const cache = new Map();
4
+ function matchPattern(pattern, name) {
5
+ if (pattern.startsWith('*.')) {
6
+ return name.endsWith(pattern.slice(1));
7
+ }
8
+ if (pattern.startsWith('*.') && pattern.includes('/')) {
9
+ return name.endsWith(pattern.split('/').pop());
10
+ }
11
+ if (pattern.includes('*')) {
12
+ const re = new RegExp('^' + pattern.replace(/\*/g, '.*').replace(/\?/g, '.') + '$');
13
+ return re.test(name);
14
+ }
15
+ return name === pattern;
16
+ }
17
+ export async function createGitignoreFilter(rootDir) {
18
+ const gitignorePath = join(rootDir, '.gitignore');
19
+ let mtime = 0;
20
+ try {
21
+ const { stat } = await import('node:fs/promises');
22
+ mtime = (await stat(gitignorePath)).mtimeMs;
23
+ }
24
+ catch {
25
+ // no .gitignore
26
+ }
27
+ const cached = cache.get(rootDir);
28
+ if (cached && cached.mtime === mtime)
29
+ return cached.filter;
30
+ let patterns = [];
31
+ try {
32
+ const content = await readFile(gitignorePath, 'utf-8');
33
+ patterns = content.split('\n')
34
+ .map(l => l.trim())
35
+ .filter(l => l && !l.startsWith('#'))
36
+ .map(raw => {
37
+ const negated = raw.startsWith('!');
38
+ let p = negated ? raw.slice(1) : raw;
39
+ const dirOnly = p.endsWith('/');
40
+ if (dirOnly)
41
+ p = p.slice(0, -1);
42
+ // extract the last segment for name matching
43
+ const namePart = p.includes('/') ? p.split('/').filter(Boolean).pop() : p;
44
+ return { raw: namePart, dirOnly, negated, fullPath: p };
45
+ });
46
+ }
47
+ catch {
48
+ // no .gitignore
49
+ }
50
+ const filter = {
51
+ isIgnored(relPath, name, isDir) {
52
+ for (const p of patterns) {
53
+ if (p.dirOnly && !isDir)
54
+ continue;
55
+ if (matchPattern(p.raw, name) || matchPattern(p.fullPath, relPath)) {
56
+ return !p.negated;
57
+ }
58
+ }
59
+ return false;
60
+ },
61
+ };
62
+ cache.set(rootDir, { mtime, filter });
63
+ return filter;
64
+ }
65
+ //# sourceMappingURL=gitignore.js.map
@@ -38,9 +38,10 @@ export async function retryIssue(workspaceId, issueId, ctx, options = {}) {
38
38
  ctx.broadcast('issue.updated', paused);
39
39
  return { issue: paused, retried: false, reason: 'issue retry limit reached' };
40
40
  }
41
+ const RETRYABLE_STATUSES = ['failed', 'running', 'reviewing', 'retrying', 'waiting_review'];
41
42
  const issueTasks = taskService.list(workspaceId, issueId);
42
43
  for (const task of issueTasks) {
43
- if (task.status !== 'failed')
44
+ if (!RETRYABLE_STATUSES.includes(task.status))
44
45
  continue;
45
46
  const reset = taskService.resetForRetry(workspaceId, task.id, { resetRetryCount: true });
46
47
  if (!reset)
@@ -59,7 +59,7 @@ export function create(workspaceId, input) {
59
59
  channelId: channel.id,
60
60
  title: input.title,
61
61
  description: input.description,
62
- status: 'draft',
62
+ status: input.status ?? 'draft',
63
63
  tasks: [],
64
64
  members: input.members || [],
65
65
  workflowId: input.workflowId,
@@ -83,7 +83,7 @@ export function createForChannel(workspaceId, channelId, input) {
83
83
  channelId,
84
84
  title: input.title,
85
85
  description: input.description,
86
- status: 'draft',
86
+ status: input.status ?? 'draft',
87
87
  tasks: [],
88
88
  members: [],
89
89
  retryCount: 0,
@@ -4,6 +4,7 @@ import { createAgentRuntime } from '../../adapters/agent-runtime.js';
4
4
  import { getThinkingRuntimeConfig } from '../llm-model-config.js';
5
5
  import { runIssueAutomation } from '../../agents/issue-agent-runner.js';
6
6
  import { publishWorkspaceEvent } from './events.js';
7
+ import { prependPersistentAgentContext } from '../persistent-agent-context.js';
7
8
  export function getConfiguredBotAgent(workspaceId) {
8
9
  const workspace = workspaceService.getById(workspaceId);
9
10
  const botAgentId = workspace?.notificationSettings?.botAgentId;
@@ -25,8 +26,15 @@ export async function runBotAgent(workspaceId, preset, message) {
25
26
  ...getThinkingRuntimeConfig(preset),
26
27
  });
27
28
  const workingDir = agentService.resolveWorkingDir(workspaceId, preset);
29
+ const workspace = workspaceService.getById(workspaceId);
28
30
  try {
29
- const result = await runtime.execute(buildBotPrompt(message), workingDir, {
31
+ const result = await runtime.execute(prependPersistentAgentContext(buildBotPrompt(message), {
32
+ workspaceId,
33
+ workingDir,
34
+ boundDirs: workspace?.boundDirs,
35
+ includeWorkspacePrompt: false,
36
+ excludeNativeClaudeMd: preset.runtimeKind === 'claude-code',
37
+ }), workingDir, {
30
38
  maxTurns: 20,
31
39
  mcpServers: agentService.getMcpServers(preset.mcps),
32
40
  skills: agentService.getAvailableSkillNames(agentService.getAgentConfigDir(workspaceId, preset), preset.skills),
@@ -0,0 +1,155 @@
1
+ import { existsSync, realpathSync, readFileSync, statSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
4
+ import { readWorkspacePrompt } from './workspace-prompt.js';
5
+ const INSTRUCTION_FILENAMES = ['CLAUDE.md', 'claude.md', 'AGENTS.md', 'agents.md'];
6
+ const MAX_FILE_CHARS = 48_000;
7
+ const MAX_TOTAL_CHARS = 120_000;
8
+ export function prependPersistentAgentContext(prompt, options) {
9
+ const context = buildPersistentAgentContext(options);
10
+ if (!context)
11
+ return prompt;
12
+ return `${context}\n\n${prompt}`;
13
+ }
14
+ export function buildPersistentAgentContext(options) {
15
+ const sections = [];
16
+ const instructionContext = buildInstructionFileContext(options.workingDir, options.boundDirs, {
17
+ excludeNativeClaudeMd: options.excludeNativeClaudeMd,
18
+ });
19
+ if (instructionContext)
20
+ sections.push(instructionContext);
21
+ if (options.includeWorkspacePrompt !== false) {
22
+ const workspacePrompt = readWorkspacePrompt(options.workspaceId).trim();
23
+ if (workspacePrompt) {
24
+ sections.push([
25
+ 'Workspace prompt:',
26
+ workspacePrompt,
27
+ ].join('\n'));
28
+ }
29
+ }
30
+ return sections.join('\n\n');
31
+ }
32
+ function buildInstructionFileContext(workingDir, boundDirs, options = {}) {
33
+ const files = collectInstructionFiles(workingDir, boundDirs, options);
34
+ if (files.length === 0)
35
+ return '';
36
+ const lines = [
37
+ 'Persistent agent instructions:',
38
+ 'The following CLAUDE.md and AGENTS.md files were auto-loaded. Earlier files are lower priority; later files are closer to the current working directory and take precedence.',
39
+ ];
40
+ let usedChars = 0;
41
+ for (const file of files) {
42
+ if (usedChars >= MAX_TOTAL_CHARS) {
43
+ lines.push('', '[Additional instruction files omitted because the persistent context budget was reached.]');
44
+ break;
45
+ }
46
+ const remaining = MAX_TOTAL_CHARS - usedChars;
47
+ const content = truncateForBudget(file.content, Math.min(MAX_FILE_CHARS, remaining));
48
+ usedChars += content.length;
49
+ lines.push('', `--- ${file.label} ---`, content);
50
+ }
51
+ return lines.join('\n').trim();
52
+ }
53
+ function collectInstructionFiles(workingDir, boundDirs, options = {}) {
54
+ const cwd = resolve(workingDir || process.cwd());
55
+ const seen = new Set();
56
+ const files = [];
57
+ const filenames = getInstructionFilenames(options);
58
+ for (const globalPath of globalInstructionPaths(options)) {
59
+ addInstructionFile(files, seen, globalPath, compactPath(globalPath));
60
+ }
61
+ for (const dir of ancestorDirs(cwd, resolveInstructionRoot(cwd, boundDirs))) {
62
+ for (const filename of filenames) {
63
+ const fullPath = join(dir, filename);
64
+ addInstructionFile(files, seen, fullPath, relativeOrAbsolute(cwd, fullPath));
65
+ }
66
+ }
67
+ return files;
68
+ }
69
+ function globalInstructionPaths(options) {
70
+ const claudeDir = join(homedir(), '.claude');
71
+ return getInstructionFilenames(options).map((filename) => join(claudeDir, filename));
72
+ }
73
+ function getInstructionFilenames(options) {
74
+ if (!options.excludeNativeClaudeMd)
75
+ return INSTRUCTION_FILENAMES;
76
+ return INSTRUCTION_FILENAMES.filter((filename) => filename !== 'CLAUDE.md');
77
+ }
78
+ function resolveInstructionRoot(cwd, boundDirs) {
79
+ const roots = (boundDirs ?? [])
80
+ .map((dir) => safeResolve(dir))
81
+ .filter((dir) => Boolean(dir))
82
+ .filter((dir) => isPathWithin(cwd, dir))
83
+ .sort((a, b) => a.length - b.length);
84
+ if (roots.length > 0)
85
+ return roots[0];
86
+ return findNearestProjectRoot(cwd);
87
+ }
88
+ function findNearestProjectRoot(cwd) {
89
+ let current = cwd;
90
+ while (true) {
91
+ if (existsSync(join(current, '.git')) || existsSync(join(current, 'package.json'))) {
92
+ return current;
93
+ }
94
+ const parent = dirname(current);
95
+ if (parent === current)
96
+ return cwd;
97
+ current = parent;
98
+ }
99
+ }
100
+ function ancestorDirs(cwd, root) {
101
+ const dirs = [];
102
+ let current = cwd;
103
+ while (true) {
104
+ dirs.push(current);
105
+ if (current === root)
106
+ break;
107
+ const parent = dirname(current);
108
+ if (parent === current)
109
+ break;
110
+ current = parent;
111
+ }
112
+ return dirs.reverse();
113
+ }
114
+ function addInstructionFile(files, seen, path, label) {
115
+ if (!existsSync(path))
116
+ return;
117
+ const stat = statSync(path);
118
+ if (!stat.isFile())
119
+ return;
120
+ const realPath = realpathSync(path);
121
+ if (seen.has(realPath))
122
+ return;
123
+ seen.add(realPath);
124
+ files.push({
125
+ path: realPath,
126
+ label,
127
+ content: readFileSync(path, 'utf-8').trim(),
128
+ });
129
+ }
130
+ function truncateForBudget(content, maxChars) {
131
+ if (content.length <= maxChars)
132
+ return content;
133
+ const suffix = '\n\n[Instruction file truncated because the persistent context budget was reached.]';
134
+ return `${content.slice(0, Math.max(0, maxChars - suffix.length)).trimEnd()}${suffix}`;
135
+ }
136
+ function safeResolve(path) {
137
+ if (!path.trim())
138
+ return null;
139
+ return resolve(isAbsolute(path) ? path : join(process.cwd(), path));
140
+ }
141
+ function isPathWithin(path, root) {
142
+ const rel = relative(root, path);
143
+ return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
144
+ }
145
+ function relativeOrAbsolute(fromDir, path) {
146
+ const rel = relative(fromDir, path);
147
+ if (!rel.startsWith('..') && !isAbsolute(rel))
148
+ return rel || path;
149
+ return path;
150
+ }
151
+ function compactPath(path) {
152
+ const home = homedir();
153
+ return path.startsWith(home) ? `~${path.slice(home.length)}` : path;
154
+ }
155
+ //# sourceMappingURL=persistent-agent-context.js.map
@@ -1,6 +1,7 @@
1
1
  import pty from 'node-pty';
2
2
  import { v4 as uuid } from 'uuid';
3
- const MAX_BUFFER_LINES = 100;
3
+ const MAX_BUFFER_LINES = 1000;
4
+ const DEFAULT_READ_LIMIT = 100;
4
5
  const sessions = new Map();
5
6
  export function createSession(workspaceId, cwd, onOutput, onExit, shell, env) {
6
7
  const id = uuid();
@@ -19,12 +20,13 @@ export function createSession(workspaceId, cwd, onOutput, onExit, shell, env) {
19
20
  cwd,
20
21
  env: ptyEnv,
21
22
  });
22
- const session = { id, pty: ptyProcess, workspaceId, cwd, buffer: [] };
23
+ const session = { id, pty: ptyProcess, workspaceId, cwd, buffer: [], output: '' };
23
24
  ptyProcess.onData((data) => {
24
25
  session.buffer.push(data);
25
26
  if (session.buffer.length > MAX_BUFFER_LINES) {
26
27
  session.buffer = session.buffer.slice(-MAX_BUFFER_LINES);
27
28
  }
29
+ session.output = trimOutputLines(session.output + data, MAX_BUFFER_LINES);
28
30
  onOutput(id, data);
29
31
  });
30
32
  ptyProcess.onExit(({ exitCode }) => onExit(id, exitCode ?? 0));
@@ -64,4 +66,47 @@ export function getSessionsByWorkspace(workspaceId) {
64
66
  }
65
67
  return result;
66
68
  }
69
+ export function readSessionOutput(workspaceId, sessionId, options = {}) {
70
+ const session = sessions.get(sessionId);
71
+ if (!session || session.workspaceId !== workspaceId) {
72
+ throw new Error(`Terminal session not found: ${sessionId}`);
73
+ }
74
+ const lines = normalizeOutput(session.output).split('\n');
75
+ const totalLines = lines.length === 1 && lines[0] === '' ? 0 : lines.length;
76
+ const limit = normalizePositiveInteger(options.limit, DEFAULT_READ_LIMIT);
77
+ const offset = normalizeNonNegativeInteger(options.offset);
78
+ const start = Math.max(totalLines - offset - limit, 0);
79
+ const end = Math.max(totalLines - offset, 0);
80
+ const page = totalLines === 0 ? [] : lines.slice(start, end);
81
+ return {
82
+ sessionId,
83
+ offset,
84
+ limit,
85
+ totalLines,
86
+ lines: page,
87
+ text: page.join('\n'),
88
+ };
89
+ }
90
+ function normalizeOutput(output) {
91
+ return stripAnsi(output).replace(/\r\n/g, '\n').replace(/\r/g, '\n').replace(/\n$/, '');
92
+ }
93
+ function trimOutputLines(output, maxLines) {
94
+ const lines = output.split(/\r\n|\n|\r/);
95
+ if (lines.length <= maxLines)
96
+ return output;
97
+ return lines.slice(-maxLines).join('\n');
98
+ }
99
+ function normalizePositiveInteger(value, fallback) {
100
+ if (typeof value !== 'number' || !Number.isFinite(value))
101
+ return fallback;
102
+ return Math.min(MAX_BUFFER_LINES, Math.max(1, Math.floor(value)));
103
+ }
104
+ function normalizeNonNegativeInteger(value) {
105
+ if (typeof value !== 'number' || !Number.isFinite(value))
106
+ return 0;
107
+ return Math.max(0, Math.floor(value));
108
+ }
109
+ function stripAnsi(value) {
110
+ return value.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1B\\))/g, '');
111
+ }
67
112
  //# sourceMappingURL=pty.js.map