@agent-spaces/server 0.3.6 → 0.3.62

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 (165) hide show
  1. package/dist/adapters/claude-code-runtime/index.js +19 -0
  2. package/dist/adapters/claude-code-runtime/sdk-config.js +34 -10
  3. package/dist/adapters/git.js +29 -13
  4. package/dist/agents/agent-message-parts.js +27 -3
  5. package/dist/package.json +1 -1
  6. package/dist/routes/channel.js +21 -4
  7. package/dist/routes/file.js +87 -1
  8. package/dist/routes/skill.js +26 -3
  9. package/dist/services/agent.js +45 -16
  10. package/dist/services/persistent-agent-context.js +37 -4
  11. package/dist/services/skill.js +163 -41
  12. package/dist/web/404.html +1 -1
  13. package/dist/web/__next.__PAGE__.txt +4 -4
  14. package/dist/web/__next._full.txt +23 -22
  15. package/dist/web/__next._head.txt +4 -4
  16. package/dist/web/__next._index.txt +13 -13
  17. package/dist/web/__next._tree.txt +2 -2
  18. package/dist/web/_next/static/chunks/{0_-c.y2qq4ax4.js → 0-81wi7xr2nd-.js} +56 -56
  19. package/dist/web/_next/static/chunks/00lca9lsasdm7.js +1 -0
  20. package/dist/web/_next/static/chunks/{0sz8_nijfiz0p.js → 02shswpmybici.js} +1 -1
  21. package/dist/web/_next/static/chunks/05f8cnmzqd5wb.js +1 -0
  22. package/dist/web/_next/static/chunks/05tx6hzdtkgi9.js +2 -0
  23. package/dist/web/_next/static/chunks/{0w1iudd1d46sw.js → 07a0rktf_nr5h.js} +1 -1
  24. package/dist/web/_next/static/chunks/{0_g3irbniww6d.js → 07nbyndvzftr1.js} +1 -1
  25. package/dist/web/_next/static/chunks/08kfx8on9oty~.js +1 -0
  26. package/dist/web/_next/static/chunks/{13ess~4pdmxma.js → 0bn~onkv~9xza.js} +1 -1
  27. package/dist/web/_next/static/chunks/0i51q.5s_-.vh.css +1 -0
  28. package/dist/web/_next/static/chunks/0k.8qdk7oi_os.js +1 -0
  29. package/dist/web/_next/static/chunks/0qf163pbrwy4z.js +1 -0
  30. package/dist/web/_next/static/chunks/{05iy4.c-1cfhd.js → 0rke_patcovgr.js} +1 -1
  31. package/dist/web/_next/static/chunks/0rnt-f3pjxb-d.js +5 -0
  32. package/dist/web/_next/static/chunks/0tgbcgsca9mrg.js +1 -0
  33. package/dist/web/_next/static/chunks/0tyhv6.y7~yxd.js +1 -0
  34. package/dist/web/_next/static/chunks/0vuvql8rloda5.js +1 -0
  35. package/dist/web/_next/static/chunks/0xmmk3vebw8xs.js +1 -0
  36. package/dist/web/_next/static/chunks/0yo22_w53mo~5.js +1 -0
  37. package/dist/web/_next/static/chunks/12c0jp0qvlr1t.js +1 -0
  38. package/dist/web/_next/static/chunks/12pg1nn3v~~5k.js +1 -0
  39. package/dist/web/_next/static/chunks/16cyr8ilpx1nn.js +1 -0
  40. package/dist/web/_not-found/__next._full.txt +19 -19
  41. package/dist/web/_not-found/__next._head.txt +4 -4
  42. package/dist/web/_not-found/__next._index.txt +13 -13
  43. package/dist/web/_not-found/__next._not-found.__PAGE__.txt +2 -2
  44. package/dist/web/_not-found/__next._not-found.txt +3 -3
  45. package/dist/web/_not-found/__next._tree.txt +2 -2
  46. package/dist/web/_not-found.html +1 -1
  47. package/dist/web/_not-found.txt +19 -19
  48. package/dist/web/icons/file_type_audio.svg +3 -1
  49. package/dist/web/icons/file_type_video.svg +10 -1
  50. package/dist/web/index.html +1 -1
  51. package/dist/web/index.txt +23 -22
  52. package/dist/web/login/__next._full.txt +24 -22
  53. package/dist/web/login/__next._head.txt +4 -4
  54. package/dist/web/login/__next._index.txt +13 -13
  55. package/dist/web/login/__next._tree.txt +2 -2
  56. package/dist/web/login/__next.login.__PAGE__.txt +4 -4
  57. package/dist/web/login/__next.login.txt +3 -3
  58. package/dist/web/login.html +1 -1
  59. package/dist/web/login.txt +24 -22
  60. package/dist/web/settings/__next._full.txt +30 -29
  61. package/dist/web/settings/__next._head.txt +4 -4
  62. package/dist/web/settings/__next._index.txt +13 -13
  63. package/dist/web/settings/__next._tree.txt +2 -2
  64. package/dist/web/settings/__next.settings.__PAGE__.txt +4 -4
  65. package/dist/web/settings/__next.settings.txt +5 -5
  66. package/dist/web/settings/agents/__next._full.txt +35 -34
  67. package/dist/web/settings/agents/__next._head.txt +4 -4
  68. package/dist/web/settings/agents/__next._index.txt +13 -13
  69. package/dist/web/settings/agents/__next._tree.txt +2 -2
  70. package/dist/web/settings/agents/__next.settings.agents.__PAGE__.txt +4 -4
  71. package/dist/web/settings/agents/__next.settings.agents.txt +3 -3
  72. package/dist/web/settings/agents/__next.settings.txt +5 -5
  73. package/dist/web/settings/agents.html +1 -1
  74. package/dist/web/settings/agents.txt +35 -34
  75. package/dist/web/settings/mcps/__next._full.txt +35 -34
  76. package/dist/web/settings/mcps/__next._head.txt +4 -4
  77. package/dist/web/settings/mcps/__next._index.txt +13 -13
  78. package/dist/web/settings/mcps/__next._tree.txt +2 -2
  79. package/dist/web/settings/mcps/__next.settings.mcps.__PAGE__.txt +4 -4
  80. package/dist/web/settings/mcps/__next.settings.mcps.txt +3 -3
  81. package/dist/web/settings/mcps/__next.settings.txt +5 -5
  82. package/dist/web/settings/mcps.html +1 -1
  83. package/dist/web/settings/mcps.txt +35 -34
  84. package/dist/web/settings/models/__next._full.txt +35 -34
  85. package/dist/web/settings/models/__next._head.txt +4 -4
  86. package/dist/web/settings/models/__next._index.txt +13 -13
  87. package/dist/web/settings/models/__next._tree.txt +2 -2
  88. package/dist/web/settings/models/__next.settings.models.__PAGE__.txt +4 -4
  89. package/dist/web/settings/models/__next.settings.models.txt +3 -3
  90. package/dist/web/settings/models/__next.settings.txt +5 -5
  91. package/dist/web/settings/models.html +1 -1
  92. package/dist/web/settings/models.txt +35 -34
  93. package/dist/web/settings/prompts/__next._full.txt +35 -34
  94. package/dist/web/settings/prompts/__next._head.txt +4 -4
  95. package/dist/web/settings/prompts/__next._index.txt +13 -13
  96. package/dist/web/settings/prompts/__next._tree.txt +2 -2
  97. package/dist/web/settings/prompts/__next.settings.prompts.__PAGE__.txt +4 -4
  98. package/dist/web/settings/prompts/__next.settings.prompts.txt +3 -3
  99. package/dist/web/settings/prompts/__next.settings.txt +5 -5
  100. package/dist/web/settings/prompts.html +1 -1
  101. package/dist/web/settings/prompts.txt +35 -34
  102. package/dist/web/settings/providers/__next._full.txt +35 -34
  103. package/dist/web/settings/providers/__next._head.txt +4 -4
  104. package/dist/web/settings/providers/__next._index.txt +13 -13
  105. package/dist/web/settings/providers/__next._tree.txt +2 -2
  106. package/dist/web/settings/providers/__next.settings.providers.__PAGE__.txt +4 -4
  107. package/dist/web/settings/providers/__next.settings.providers.txt +3 -3
  108. package/dist/web/settings/providers/__next.settings.txt +5 -5
  109. package/dist/web/settings/providers.html +1 -1
  110. package/dist/web/settings/providers.txt +35 -34
  111. package/dist/web/settings/skills/__next._full.txt +35 -34
  112. package/dist/web/settings/skills/__next._head.txt +4 -4
  113. package/dist/web/settings/skills/__next._index.txt +13 -13
  114. package/dist/web/settings/skills/__next._tree.txt +2 -2
  115. package/dist/web/settings/skills/__next.settings.skills.__PAGE__.txt +4 -4
  116. package/dist/web/settings/skills/__next.settings.skills.txt +3 -3
  117. package/dist/web/settings/skills/__next.settings.txt +5 -5
  118. package/dist/web/settings/skills.html +1 -1
  119. package/dist/web/settings/skills.txt +35 -34
  120. package/dist/web/settings.html +1 -1
  121. package/dist/web/settings.txt +30 -29
  122. package/dist/web/workflows/__next._full.txt +31 -30
  123. package/dist/web/workflows/__next._head.txt +4 -4
  124. package/dist/web/workflows/__next._index.txt +13 -13
  125. package/dist/web/workflows/__next._tree.txt +2 -2
  126. package/dist/web/workflows/__next.workflows.__PAGE__.txt +4 -4
  127. package/dist/web/workflows/__next.workflows.txt +3 -3
  128. package/dist/web/workflows.html +1 -1
  129. package/dist/web/workflows.txt +31 -30
  130. package/dist/web/workspace/_/__next._full.txt +56 -55
  131. package/dist/web/workspace/_/__next._head.txt +4 -4
  132. package/dist/web/workspace/_/__next._index.txt +13 -13
  133. package/dist/web/workspace/_/__next._tree.txt +2 -2
  134. package/dist/web/workspace/_/__next.workspace.$d$id.__PAGE__.txt +3 -3
  135. package/dist/web/workspace/_/__next.workspace.$d$id.txt +3 -3
  136. package/dist/web/workspace/_/__next.workspace.txt +3 -3
  137. package/dist/web/workspace/_.html +1 -1
  138. package/dist/web/workspace/_.txt +56 -55
  139. package/dist/web/workspaces/__next._full.txt +30 -27
  140. package/dist/web/workspaces/__next._head.txt +4 -4
  141. package/dist/web/workspaces/__next._index.txt +13 -13
  142. package/dist/web/workspaces/__next._tree.txt +2 -2
  143. package/dist/web/workspaces/__next.workspaces.__PAGE__.txt +4 -4
  144. package/dist/web/workspaces/__next.workspaces.txt +3 -3
  145. package/dist/web/workspaces.html +1 -1
  146. package/dist/web/workspaces.txt +30 -27
  147. package/dist/ws/agent-runner.js +51 -31
  148. package/dist/ws/message-parts.js +61 -2
  149. package/package.json +10 -11
  150. package/dist/web/_next/static/chunks/0-1pn22sg~67u.js +0 -2
  151. package/dist/web/_next/static/chunks/03me5_ir..2ix.js +0 -1
  152. package/dist/web/_next/static/chunks/05eu2fdixf7c9.js +0 -1
  153. package/dist/web/_next/static/chunks/0cs830n97dgjy.js +0 -1
  154. package/dist/web/_next/static/chunks/0ju52ko_nh7gi.js +0 -1
  155. package/dist/web/_next/static/chunks/0mny8i81aenx3.js +0 -1
  156. package/dist/web/_next/static/chunks/0nsu~f3voe~0i.js +0 -5
  157. package/dist/web/_next/static/chunks/0w3o.7vi.dlm6.js +0 -1
  158. package/dist/web/_next/static/chunks/0x4b-a2ixww6m.js +0 -1
  159. package/dist/web/_next/static/chunks/0~w6ugk9s5p-h.css +0 -1
  160. package/dist/web/_next/static/chunks/125nunjfj2fj0.js +0 -1
  161. package/dist/web/_next/static/chunks/1285yqy_oirb-.js +0 -1
  162. package/dist/web/_next/static/chunks/14n8i2xz4_y-e.js +0 -1
  163. /package/dist/web/_next/static/{HUlYEknPhyW7dRmJ5mFdG → GS3b40oeaeLxm7i9-ibaj}/_buildManifest.js +0 -0
  164. /package/dist/web/_next/static/{HUlYEknPhyW7dRmJ5mFdG → GS3b40oeaeLxm7i9-ibaj}/_clientMiddlewareManifest.js +0 -0
  165. /package/dist/web/_next/static/{HUlYEknPhyW7dRmJ5mFdG → GS3b40oeaeLxm7i9-ibaj}/_ssgManifest.js +0 -0
@@ -84,9 +84,12 @@ export class ClaudeCodeRuntime {
84
84
  options?.onEvent?.({ type: 'session', sessionId });
85
85
  }
86
86
  const toolUses = extractToolUseEvents(message);
87
+ let sawAskUserQuestion = false;
87
88
  for (const toolUse of toolUses) {
88
89
  if (toolUse.name === 'AskUserQuestion') {
89
90
  pendingAskUserQuestionToolIds.add(toolUse.id);
91
+ waitingForUserAnswer = true;
92
+ sawAskUserQuestion = true;
90
93
  }
91
94
  }
92
95
  const toolResult = extractToolResultEvent(message);
@@ -133,8 +136,24 @@ export class ClaudeCodeRuntime {
133
136
  error = message.errors.join('\n') || message.subtype;
134
137
  }
135
138
  }
139
+ if (sawAskUserQuestion)
140
+ break;
136
141
  }
137
142
  const elapsed = Date.now() - startTime;
143
+ if (waitingForUserAnswer) {
144
+ d(`waiting for user answer ${elapsed}ms | turns=${turns} tokens=${tokenCount}`);
145
+ if (usageLine)
146
+ output.push(usageLine);
147
+ return {
148
+ success: true,
149
+ summary: 'Waiting for user answer',
150
+ artifacts: [],
151
+ output,
152
+ usage,
153
+ costUsd,
154
+ sessionId,
155
+ };
156
+ }
138
157
  if (!sawResult) {
139
158
  const runtimeError = extractRuntimeError([...stderrLines, ...output])
140
159
  || 'Claude Code execution stopped before reporting a final result';
@@ -1,6 +1,6 @@
1
- import { copyFileSync, existsSync, mkdirSync, readdirSync, rmSync, statSync } from 'node:fs';
1
+ import { copyFileSync, cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
2
2
  import { createRequire } from 'node:module';
3
- import { extname, isAbsolute, resolve } from 'node:path';
3
+ import { basename, extname, isAbsolute, resolve } from 'node:path';
4
4
  import { join } from 'node:path';
5
5
  import { createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk';
6
6
  import { isAnthropicBridgeProvider } from './types.js';
@@ -125,7 +125,7 @@ export function normalizeSkillNames(skills, configDir) {
125
125
  if (!Array.isArray(skills))
126
126
  return [];
127
127
  return skills
128
- .map((skill) => skill.trim().replace(/\.md$/i, ''))
128
+ .map(sanitizeSkillName)
129
129
  .filter((skill) => Boolean(skill) && hasSkillContent(skill, configDir));
130
130
  }
131
131
  export function prepareConfigDir(configDir, agentDir) {
@@ -138,19 +138,43 @@ export function prepareConfigDir(configDir, agentDir) {
138
138
  mkdirSync(targetSkillsDir, { recursive: true });
139
139
  if (!existsSync(sourceSkillsDir))
140
140
  return;
141
- for (const file of readdirSync(sourceSkillsDir)) {
142
- if (extname(file).toLowerCase() !== '.md')
141
+ for (const entry of readdirSync(sourceSkillsDir)) {
142
+ const source = join(sourceSkillsDir, entry);
143
+ const sourceStat = statSync(source);
144
+ if (sourceStat.isDirectory()) {
145
+ const skillName = sanitizeSkillName(entry);
146
+ if (!skillName)
147
+ continue;
148
+ const sourceSkillFile = join(source, 'SKILL.md');
149
+ const targetSkillDir = join(targetSkillsDir, skillName);
150
+ if (existsSync(sourceSkillFile) && statSync(sourceSkillFile).size > 0) {
151
+ cpSync(source, targetSkillDir, { recursive: true, force: true });
152
+ }
143
153
  continue;
144
- copyFileSync(join(sourceSkillsDir, file), join(targetSkillsDir, file));
154
+ }
155
+ if (!sourceStat.isFile() || extname(entry).toLowerCase() !== '.md')
156
+ continue;
157
+ const skillName = sanitizeSkillName(entry);
158
+ if (!skillName || sourceStat.size === 0)
159
+ continue;
160
+ const targetSkillDir = join(targetSkillsDir, skillName);
161
+ mkdirSync(targetSkillDir, { recursive: true });
162
+ writeFileSync(join(targetSkillDir, 'SKILL.md'), readFileSync(source, 'utf-8'), 'utf-8');
163
+ copyFileSync(source, join(targetSkillsDir, `${skillName}.md`));
145
164
  }
146
165
  }
147
166
  function hasSkillContent(skill, configDir) {
148
167
  if (!configDir)
149
168
  return true;
150
- const skillFile = join(configDir, 'skills', `${skill}.md`);
151
- if (!existsSync(skillFile))
152
- return false;
153
- return statSync(skillFile).size > 0;
169
+ const skillFile = join(configDir, 'skills', skill, 'SKILL.md');
170
+ if (existsSync(skillFile) && statSync(skillFile).size > 0)
171
+ return true;
172
+ const legacySkillFile = join(configDir, 'skills', `${skill}.md`);
173
+ return existsSync(legacySkillFile) && statSync(legacySkillFile).size > 0;
174
+ }
175
+ function sanitizeSkillName(name) {
176
+ const raw = basename(name).replace(/\.md$/i, '').trim();
177
+ return raw.replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');
154
178
  }
155
179
  export function normalizePermissionMode(permissionMode) {
156
180
  if (permissionMode === 'default' ||
@@ -3,6 +3,19 @@ import { getWorkspace } from '../storage/workspace-store.js';
3
3
  import { writeFile, readFile, mkdir } from 'node:fs/promises';
4
4
  import { existsSync } from 'node:fs';
5
5
  import { join, basename } from 'node:path';
6
+ const BINARY_EXTENSIONS = new Set([
7
+ 'png', 'jpg', 'jpeg', 'gif', 'bmp', 'ico', 'webp', 'svg', 'tiff', 'tif',
8
+ 'mp3', 'mp4', 'wav', 'avi', 'mov', 'mkv', 'flv', 'wmv', 'webm',
9
+ 'zip', 'tar', 'gz', 'bz2', 'xz', '7z', 'rar', 'dmg', 'iso',
10
+ 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx',
11
+ 'woff', 'woff2', 'ttf', 'otf', 'eot',
12
+ 'exe', 'dll', 'so', 'dylib', 'bin', 'dat',
13
+ 'sqlite', 'db', 'pyc', 'o', 'obj', 'class', 'jar', 'wasm',
14
+ ]);
15
+ function isBinaryPath(filePath) {
16
+ const ext = filePath.split('.').pop()?.toLowerCase();
17
+ return ext ? BINARY_EXTENSIONS.has(ext) : true;
18
+ }
6
19
  const gitInstances = new Map();
7
20
  function getGitOptions() {
8
21
  const proxy = process.env.HTTPS_PROXY || process.env.https_proxy
@@ -115,9 +128,9 @@ export async function gitDiff(workspaceId, filePath) {
115
128
  const newContent = await import('node:fs/promises').then(fs => fs.readFile(`${ws.boundDirs[0]}/${filePath}`, 'utf-8')).catch(() => '');
116
129
  diffs.push({
117
130
  path: filePath,
118
- oldContent,
119
- newContent,
120
- isBinary: false,
131
+ oldContent: isBinaryPath(filePath) ? '' : oldContent,
132
+ newContent: isBinaryPath(filePath) ? '' : newContent,
133
+ isBinary: isBinaryPath(filePath),
121
134
  isNew: !oldContent,
122
135
  isDeleted: !newContent,
123
136
  });
@@ -126,13 +139,14 @@ export async function gitDiff(workspaceId, filePath) {
126
139
  const raw = await git.diff(['--name-only']);
127
140
  const files = raw.split('\n').filter(Boolean);
128
141
  for (const f of files) {
129
- const oldContent = await git.show([`HEAD:${f}`]).catch(() => '');
130
- const newContent = await import('node:fs/promises').then(fs => fs.readFile(`${ws.boundDirs[0]}/${f}`, 'utf-8')).catch(() => '');
142
+ const binary = isBinaryPath(f);
143
+ const oldContent = binary ? '' : await git.show([`HEAD:${f}`]).catch(() => '');
144
+ const newContent = binary ? '' : await import('node:fs/promises').then(fs => fs.readFile(`${ws.boundDirs[0]}/${f}`, 'utf-8')).catch(() => '');
131
145
  diffs.push({
132
146
  path: f,
133
147
  oldContent,
134
148
  newContent,
135
- isBinary: false,
149
+ isBinary: binary,
136
150
  isNew: !oldContent,
137
151
  isDeleted: !newContent,
138
152
  });
@@ -140,13 +154,14 @@ export async function gitDiff(workspaceId, filePath) {
140
154
  // Also check untracked (staged new files)
141
155
  const status = await git.status();
142
156
  for (const f of status.not_added) {
143
- const newContent = await import('node:fs/promises').then(fs => fs.readFile(`${ws.boundDirs[0]}/${f}`, 'utf-8')).catch(() => '');
144
- if (newContent) {
157
+ const binary = isBinaryPath(f);
158
+ const newContent = binary ? '' : await import('node:fs/promises').then(fs => fs.readFile(`${ws.boundDirs[0]}/${f}`, 'utf-8')).catch(() => '');
159
+ if (newContent || binary) {
145
160
  diffs.push({
146
161
  path: f,
147
162
  oldContent: '',
148
- newContent,
149
- isBinary: false,
163
+ newContent: binary ? '' : newContent,
164
+ isBinary: binary,
150
165
  isNew: true,
151
166
  isDeleted: false,
152
167
  });
@@ -337,9 +352,10 @@ export async function gitCommitDiff(workspaceId, commitHash) {
337
352
  const files = nameOnlyRaw.split('\n').filter(Boolean);
338
353
  const diffs = [];
339
354
  for (const f of files) {
340
- const oldContent = await git.show([`${commitHash}^:${f}`]).catch(() => '');
341
- const newContent = await git.show([`${commitHash}:${f}`]).catch(() => '');
342
- diffs.push({ path: f, oldContent, newContent, isBinary: false, isNew: !oldContent, isDeleted: !newContent });
355
+ const binary = isBinaryPath(f);
356
+ const oldContent = binary ? '' : await git.show([`${commitHash}^:${f}`]).catch(() => '');
357
+ const newContent = binary ? '' : await git.show([`${commitHash}:${f}`]).catch(() => '');
358
+ diffs.push({ path: f, oldContent, newContent, isBinary: binary, isNew: !oldContent, isDeleted: !newContent });
343
359
  }
344
360
  return diffs;
345
361
  }
@@ -46,7 +46,7 @@ export function createAgentMessagePartsTracker(input) {
46
46
  output.push(event.line);
47
47
  input.onOutput?.(event.line);
48
48
  },
49
- buildParts({ sessionId, workspaceRoot, model, usage, success, error }) {
49
+ buildParts({ sessionId, workspaceRoot, model, usage, persistentContext, success, error }) {
50
50
  return buildAgentMessageParts({
51
51
  sessionId,
52
52
  workspaceRoot,
@@ -55,6 +55,7 @@ export function createAgentMessagePartsTracker(input) {
55
55
  output,
56
56
  reasoning,
57
57
  toolDetails,
58
+ persistentContext,
58
59
  success,
59
60
  error,
60
61
  });
@@ -69,7 +70,7 @@ function buildAgentMessageParts(input) {
69
70
  : '';
70
71
  const usage = input.usage ?? extractUsage(lines);
71
72
  const parts = [];
72
- const chainItems = buildChainItems(lines, finalTextRange, finalText, input.workspaceRoot, input.toolDetails);
73
+ const chainItems = buildChainItems(lines, finalTextRange, finalText, input.workspaceRoot, input.toolDetails, input.persistentContext);
73
74
  const reasoningText = normalizeReasoningText(input.reasoning);
74
75
  if (reasoningText) {
75
76
  parts.push({
@@ -181,12 +182,17 @@ function findFinalTextRange(lines) {
181
182
  }
182
183
  return { start, end };
183
184
  }
184
- function buildChainItems(lines, finalTextRange, finalText, workspaceRoot, toolDetails) {
185
+ function buildChainItems(lines, finalTextRange, finalText, workspaceRoot, toolDetails, persistentContext) {
185
186
  let toolIndex = 0;
186
187
  let messageIndex = 0;
187
188
  const items = [];
188
189
  const toolDetailMatchCounts = new Map();
189
190
  let messageBuffer = [];
191
+ const persistentContextItem = buildPersistentContextToolItem(persistentContext);
192
+ if (persistentContextItem) {
193
+ items.push(persistentContextItem);
194
+ toolIndex += 1;
195
+ }
190
196
  const flushMessageBuffer = () => {
191
197
  if (messageBuffer.length === 0)
192
198
  return;
@@ -229,6 +235,7 @@ function buildToolTodo(line, index, workspaceRoot, toolDetails, toolDetailMatchC
229
235
  const detailId = findToolDetailId(line, toolDetails, toolDetailMatchCounts);
230
236
  return {
231
237
  id: `tool-${index}`,
238
+ kind: 'tool',
232
239
  title: summary.title,
233
240
  description: summary.description,
234
241
  status: 'completed',
@@ -238,6 +245,23 @@ function buildToolTodo(line, index, workspaceRoot, toolDetails, toolDetailMatchC
238
245
  detailId,
239
246
  };
240
247
  }
248
+ function buildPersistentContextToolItem(persistentContext) {
249
+ const counts = persistentContext?.counts;
250
+ if (!counts?.total)
251
+ return null;
252
+ const description = [
253
+ counts.claudeMd ? `${counts.claudeMd} claude.md` : null,
254
+ counts.agentsMd ? `${counts.agentsMd} AGENTS.md` : null,
255
+ ].filter(Boolean).join(', ') || `${counts.total} instruction files`;
256
+ return {
257
+ id: 'tool-persistent-context',
258
+ kind: 'tool',
259
+ title: 'Load claude.md',
260
+ description,
261
+ status: 'completed',
262
+ toolName: 'LoadClaudeMd',
263
+ };
264
+ }
241
265
  function summarizeToolLine(line, workspaceRoot) {
242
266
  const trimmed = line.trim();
243
267
  const toolName = extractToolName(trimmed);
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-spaces/server",
3
- "version": "0.3.5",
3
+ "version": "0.3.62",
4
4
  "type": "module",
5
5
  "main": "app.js",
6
6
  "packageManager": "pnpm@10.17.1",
@@ -2,7 +2,7 @@ import { Router } from 'express';
2
2
  import { listChannels, createChannel, getChannel, updateChannel, deleteChannel } from '../services/channel.js';
3
3
  import { listMessages, createMessage, updateMessage, deleteMessage, clearMessages } from '../services/message.js';
4
4
  import { broadcastToWorkspace } from '../ws/connection-manager.js';
5
- import { hasActiveChannelRuns, markInactiveChannelRunsStopped, stopChannelRuns } from '../ws/agent-runner.js';
5
+ import { hasActiveChannelRuns, stopChannelRuns } from '../ws/agent-runner.js';
6
6
  import { getToolDetail } from '../services/tool-detail.js';
7
7
  import * as issueService from '../services/issue.js';
8
8
  const router = Router({ mergeParams: true });
@@ -69,11 +69,28 @@ router.get('/:channelId/messages', (req, res) => {
69
69
  res.status(404).json({ error: 'channel not found' });
70
70
  return;
71
71
  }
72
- if (!hasActiveChannelRuns(id, channelId)) {
73
- markInactiveChannelRunsStopped(id, channelId);
74
- }
75
72
  res.json(listMessages(id, channelId, { limit: limit ? Number(limit) : undefined, before: before }));
76
73
  });
74
+ // GET /api/workspaces/:id/channels/:channelId/state
75
+ router.get('/:channelId/state', (req, res) => {
76
+ const { id, channelId } = req.params;
77
+ if (!getChannel(id, channelId)) {
78
+ res.status(404).json({ error: 'channel not found' });
79
+ return;
80
+ }
81
+ const messages = listMessages(id, channelId, { limit: 1 });
82
+ const lastMessage = messages.at(-1);
83
+ res.json({
84
+ channelId,
85
+ active: hasActiveChannelRuns(id, channelId),
86
+ lastMessage: lastMessage ? {
87
+ id: lastMessage.id,
88
+ status: lastMessage.status,
89
+ metadata: lastMessage.metadata,
90
+ hasPendingQuestion: Boolean(lastMessage.parts?.some((part) => (part.type === 'ask_user_question' && part.status !== 'answered' && !part.answer))),
91
+ } : null,
92
+ });
93
+ });
77
94
  // GET /api/workspaces/:id/channels/:channelId/messages/:messageId/tool-details/:detailId
78
95
  router.get('/:channelId/messages/:messageId/tool-details/:detailId', (req, res) => {
79
96
  const { id, channelId, messageId, detailId } = req.params;
@@ -1,6 +1,7 @@
1
1
  import { Router } from 'express';
2
2
  import { exec } from 'child_process';
3
- import { resolve, join } from 'path';
3
+ import { resolve, join, extname } from 'path';
4
+ import { existsSync, createReadStream, statSync } from 'node:fs';
4
5
  import * as fileService from '../services/file.js';
5
6
  import { getDataDir } from '../storage/json-store.js';
6
7
  const EDITOR_STATE_PATH = '.agentspace/editor-state.json';
@@ -16,6 +17,13 @@ router.get('/tree', async (req, res) => {
16
17
  const tree = await fileService.readTree(ws, path, depth);
17
18
  res.json(tree);
18
19
  });
20
+ const MIME_MAP = {
21
+ '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif',
22
+ '.webp': 'image/webp', '.svg': 'image/svg+xml', '.bmp': 'image/bmp', '.ico': 'image/x-icon',
23
+ '.mp4': 'video/mp4', '.webm': 'video/webm', '.ogg': 'video/ogg', '.mov': 'video/quicktime',
24
+ '.mp3': 'audio/mpeg', '.wav': 'audio/wav', '.flac': 'audio/flac', '.aac': 'audio/aac',
25
+ '.m4a': 'audio/mp4', '.opus': 'audio/opus',
26
+ };
19
27
  router.get('/content', async (req, res) => {
20
28
  const ws = fileService.getWorkspace(req.params.id);
21
29
  if (!ws) {
@@ -27,6 +35,38 @@ router.get('/content', async (req, res) => {
27
35
  res.status(400).json({ error: 'path is required' });
28
36
  return;
29
37
  }
38
+ const raw = req.query.raw === 'true';
39
+ if (raw) {
40
+ const abs = fileService.resolvePath(ws, path);
41
+ if (!abs || !existsSync(abs)) {
42
+ res.status(404).json({ error: 'File not found' });
43
+ return;
44
+ }
45
+ const ext = extname(path).toLowerCase();
46
+ const mime = MIME_MAP[ext] || 'application/octet-stream';
47
+ const stat = statSync(abs);
48
+ const fileSize = stat.size;
49
+ // Handle Range requests for video/audio seeking
50
+ const range = req.headers.range;
51
+ if (range) {
52
+ const parts = range.replace(/bytes=/, '').split('-');
53
+ const start = parseInt(parts[0], 10);
54
+ const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1;
55
+ const chunkSize = end - start + 1;
56
+ res.status(206);
57
+ res.setHeader('Content-Range', `bytes ${start}-${end}/${fileSize}`);
58
+ res.setHeader('Accept-Ranges', 'bytes');
59
+ res.setHeader('Content-Length', chunkSize);
60
+ res.setHeader('Content-Type', mime);
61
+ createReadStream(abs, { start, end }).pipe(res);
62
+ return;
63
+ }
64
+ res.setHeader('Content-Type', mime);
65
+ res.setHeader('Accept-Ranges', 'bytes');
66
+ res.setHeader('Content-Length', fileSize);
67
+ createReadStream(abs).pipe(res);
68
+ return;
69
+ }
30
70
  const result = await fileService.readFileContent(ws, path);
31
71
  if (!result) {
32
72
  res.status(404).json({ error: 'File not found' });
@@ -253,5 +293,51 @@ router.post('/upload', async (req, res) => {
253
293
  }
254
294
  res.json({ ok: true, paths: results });
255
295
  });
296
+ router.get('/download', async (req, res) => {
297
+ const ws = fileService.getWorkspace(req.params.id);
298
+ if (!ws) {
299
+ res.status(404).json({ error: 'Workspace not found' });
300
+ return;
301
+ }
302
+ const path = req.query.path;
303
+ if (!path) {
304
+ res.status(400).json({ error: 'path is required' });
305
+ return;
306
+ }
307
+ const abs = fileService.resolvePath(ws, path);
308
+ if (!abs || !existsSync(abs)) {
309
+ res.status(404).json({ error: 'File not found' });
310
+ return;
311
+ }
312
+ const stat = statSync(abs);
313
+ if (stat.isDirectory()) {
314
+ res.status(400).json({ error: 'Cannot download a directory' });
315
+ return;
316
+ }
317
+ const ext = extname(path).toLowerCase();
318
+ const mime = MIME_MAP[ext] || 'application/octet-stream';
319
+ const fileName = path.split('/').pop() || 'download';
320
+ // Handle Range requests for large files
321
+ const range = req.headers.range;
322
+ if (range) {
323
+ const parts = range.replace(/bytes=/, '').split('-');
324
+ const start = parseInt(parts[0], 10);
325
+ const end = parts[1] ? parseInt(parts[1], 10) : stat.size - 1;
326
+ const chunkSize = end - start + 1;
327
+ res.status(206);
328
+ res.setHeader('Content-Range', `bytes ${start}-${end}/${stat.size}`);
329
+ res.setHeader('Accept-Ranges', 'bytes');
330
+ res.setHeader('Content-Length', chunkSize);
331
+ res.setHeader('Content-Type', mime);
332
+ res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(fileName)}"`);
333
+ createReadStream(abs, { start, end }).pipe(res);
334
+ return;
335
+ }
336
+ res.setHeader('Content-Type', mime);
337
+ res.setHeader('Content-Length', stat.size);
338
+ res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(fileName)}"`);
339
+ res.setHeader('Accept-Ranges', 'bytes');
340
+ createReadStream(abs).pipe(res);
341
+ });
256
342
  export default router;
257
343
  //# sourceMappingURL=file.js.map
@@ -1,5 +1,5 @@
1
1
  import { Router } from 'express';
2
- import { listSkills, importSkill, toggleFavorite, updateSkillContent, deleteSkill, checkSkillSync, syncSkills } from '../services/skill.js';
2
+ import { listSkills, importSkill, importSkillsBatch, toggleFavorite, toggleEnabled, toggleAllEnabled, updateSkillContent, deleteSkill, checkSkillSync, syncSkills } from '../services/skill.js';
3
3
  const router = Router();
4
4
  router.get('/', (_req, res) => {
5
5
  res.json(listSkills());
@@ -17,19 +17,42 @@ router.post('/sync', (req, res) => {
17
17
  res.json({ synced });
18
18
  });
19
19
  router.post('/import', (req, res) => {
20
- const { filename, content } = req.body;
20
+ const { filename, content, group } = req.body;
21
21
  if (!filename || !content) {
22
22
  res.status(400).json({ error: 'filename and content required' });
23
23
  return;
24
24
  }
25
- const skill = importSkill(filename, content);
25
+ const skill = importSkill(filename, content, group);
26
26
  res.json(skill);
27
27
  });
28
+ router.post('/import-batch', (req, res) => {
29
+ const { items } = req.body;
30
+ if (!Array.isArray(items) || items.length === 0) {
31
+ res.status(400).json({ error: 'items required' });
32
+ return;
33
+ }
34
+ const skills = importSkillsBatch(items);
35
+ res.json(skills);
36
+ });
28
37
  router.post('/:name/favorite', (req, res) => {
29
38
  const name = typeof req.params.name === 'string' ? req.params.name : req.params.name[0];
30
39
  const favorited = toggleFavorite(name);
31
40
  res.json({ favorited });
32
41
  });
42
+ router.post('/:name/toggle', (req, res) => {
43
+ const name = typeof req.params.name === 'string' ? req.params.name : req.params.name[0];
44
+ const enabled = toggleEnabled(name);
45
+ res.json({ enabled });
46
+ });
47
+ router.post('/toggle-all', (req, res) => {
48
+ const { names, enabled } = req.body;
49
+ if (!Array.isArray(names) || typeof enabled !== 'boolean') {
50
+ res.status(400).json({ error: 'names and enabled required' });
51
+ return;
52
+ }
53
+ toggleAllEnabled(names, enabled);
54
+ res.json({ success: true });
55
+ });
33
56
  router.put('/:name', (req, res) => {
34
57
  const name = typeof req.params.name === 'string' ? req.params.name : req.params.name[0];
35
58
  const { content } = req.body;
@@ -346,7 +346,13 @@ export function getAvailableSkillNames(agentDir, skills) {
346
346
  .filter((skill) => {
347
347
  if (!skill)
348
348
  return false;
349
- const skillFile = join(agentDir, 'skills', `${skill}.md`);
349
+ const skillsBase = join(agentDir, 'skills');
350
+ // Check folder structure first, then flat .md
351
+ const skillFolder = join(skillsBase, skill);
352
+ if (existsSync(skillFolder) && statSync(skillFolder).isDirectory()) {
353
+ return readdirSync(skillFolder).some((f) => f.endsWith('.md'));
354
+ }
355
+ const skillFile = join(skillsBase, `${skill}.md`);
350
356
  return existsSync(skillFile) && statSync(skillFile).size > 0;
351
357
  });
352
358
  }
@@ -448,25 +454,43 @@ function writeAgentTemplate(preset, skillInputs) {
448
454
  else if (preset.skills?.length) {
449
455
  const globalSkillsDir = join(getDataDir(), 'skills');
450
456
  const keepFiles = new Set(preset.skills.map((s) => s.endsWith('.md') ? s : `${s}.md`));
451
- // Remove skill files no longer in the list
457
+ const keepNames = new Set(preset.skills.map((s) => s.replace(/\.md$/i, '')));
458
+ // Remove skill entries no longer in the list (flat .md files and folders)
452
459
  if (existsSync(skillsDir)) {
453
460
  for (const existing of readdirSync(skillsDir)) {
454
- if (existing.endsWith('.md') && !keepFiles.has(existing)) {
455
- rmSync(join(skillsDir, existing), { force: true });
461
+ const name = existing.endsWith('.md') ? existing.replace(/\.md$/i, '') : existing;
462
+ if (!keepNames.has(name)) {
463
+ rmSync(join(skillsDir, existing), { recursive: true, force: true });
456
464
  console.log('[writeAgentTemplate] removed stale skill:', existing);
457
465
  }
458
466
  }
459
467
  }
460
- // Copy / ensure skill files
461
- for (const filename of keepFiles) {
462
- const target = join(skillsDir, filename);
463
- const globalSource = join(globalSkillsDir, filename);
464
- console.log('[writeAgentTemplate] skill:', filename, 'globalExists:', existsSync(globalSource), 'targetExists:', existsSync(target));
465
- if (existsSync(globalSource)) {
466
- copyFileSync(globalSource, target);
468
+ // Copy / ensure skill files (global skills are folders: skills/{name}/SKILL.md)
469
+ for (const skillName of keepNames) {
470
+ const globalFolder = join(globalSkillsDir, skillName);
471
+ const flatTarget = join(skillsDir, `${skillName}.md`);
472
+ const folderTarget = join(skillsDir, skillName);
473
+ console.log('[writeAgentTemplate] skill:', skillName, 'globalFolder:', globalFolder, 'globalExists:', existsSync(globalFolder) && statSync(globalFolder).isDirectory());
474
+ if (existsSync(globalFolder) && statSync(globalFolder).isDirectory()) {
475
+ // Copy entire skill folder
476
+ if (existsSync(folderTarget)) {
477
+ rmSync(folderTarget, { recursive: true, force: true });
478
+ }
479
+ cpSync(globalFolder, folderTarget, { recursive: true, force: true });
480
+ // Remove stale flat .md if it exists
481
+ if (existsSync(flatTarget)) {
482
+ rmSync(flatTarget, { force: true });
483
+ }
467
484
  }
468
- else if (!existsSync(target)) {
469
- writeFileSync(target, '', 'utf-8');
485
+ else {
486
+ // Fallback: single SKILL.md or legacy flat file
487
+ const globalSkillFile = join(globalFolder, 'SKILL.md');
488
+ if (existsSync(globalSkillFile)) {
489
+ copyFileSync(globalSkillFile, flatTarget);
490
+ }
491
+ else if (!existsSync(flatTarget) && !existsSync(folderTarget)) {
492
+ writeFileSync(flatTarget, '', 'utf-8');
493
+ }
470
494
  }
471
495
  }
472
496
  }
@@ -492,9 +516,14 @@ function copyAgentTemplateToWorkspace(agentId, agentspaceDir) {
492
516
  if (existsSync(skillsDir)) {
493
517
  const workspaceSkillsDir = join(agentspaceDir, 'skills');
494
518
  ensureDir(workspaceSkillsDir);
495
- for (const file of readdirSync(skillsDir)) {
496
- if (extname(file).toLowerCase() === '.md') {
497
- copyFileSync(join(skillsDir, file), join(workspaceSkillsDir, file));
519
+ for (const entry of readdirSync(skillsDir)) {
520
+ const source = join(skillsDir, entry);
521
+ const sourceStat = statSync(source);
522
+ if (sourceStat.isDirectory()) {
523
+ cpSync(source, join(workspaceSkillsDir, entry), { recursive: true, force: true });
524
+ }
525
+ else if (sourceStat.isFile() && extname(entry).toLowerCase() === '.md') {
526
+ copyFileSync(source, join(workspaceSkillsDir, entry));
498
527
  }
499
528
  }
500
529
  }
@@ -1,6 +1,6 @@
1
1
  import { existsSync, realpathSync, readFileSync, statSync } from 'node:fs';
2
2
  import { homedir } from 'node:os';
3
- import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
3
+ import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
4
4
  import { getDataDir } from '../storage/json-store.js';
5
5
  import { readWorkspacePrompt } from './workspace-prompt.js';
6
6
  const INSTRUCTION_FILENAMES = ['CLAUDE.md', 'claude.md', 'AGENTS.md', 'agents.md'];
@@ -13,10 +13,8 @@ export function prependPersistentAgentContext(prompt, options) {
13
13
  return `${context}\n\n${prompt}`;
14
14
  }
15
15
  export function buildPersistentAgentContext(options) {
16
+ const { instructionContext } = buildPersistentAgentContextDetails(options);
16
17
  const sections = [];
17
- const instructionContext = buildInstructionFileContext(options.workingDir, options.boundDirs, {
18
- excludeNativeClaudeMd: options.excludeNativeClaudeMd,
19
- });
20
18
  if (instructionContext)
21
19
  sections.push(instructionContext);
22
20
  if (options.includeWorkspacePrompt !== false) {
@@ -30,8 +28,20 @@ export function buildPersistentAgentContext(options) {
30
28
  }
31
29
  return sections.join('\n\n');
32
30
  }
31
+ export function buildPersistentAgentContextDetails(options) {
32
+ const instructionFiles = collectInstructionFiles(options.workingDir, options.boundDirs, {
33
+ excludeNativeClaudeMd: options.excludeNativeClaudeMd,
34
+ });
35
+ return {
36
+ instructionContext: buildInstructionFileContextFromFiles(instructionFiles),
37
+ summary: buildPersistentAgentContextSummary(instructionFiles),
38
+ };
39
+ }
33
40
  function buildInstructionFileContext(workingDir, boundDirs, options = {}) {
34
41
  const files = collectInstructionFiles(workingDir, boundDirs, options);
42
+ return buildInstructionFileContextFromFiles(files);
43
+ }
44
+ function buildInstructionFileContextFromFiles(files) {
35
45
  if (files.length === 0)
36
46
  return '';
37
47
  const lines = [
@@ -51,6 +61,28 @@ function buildInstructionFileContext(workingDir, boundDirs, options = {}) {
51
61
  }
52
62
  return lines.join('\n').trim();
53
63
  }
64
+ function buildPersistentAgentContextSummary(files) {
65
+ const instructionFiles = files.map((file) => ({
66
+ path: file.path,
67
+ label: file.label,
68
+ filename: file.filename,
69
+ }));
70
+ const counts = instructionFiles.reduce((acc, file) => {
71
+ acc.total += 1;
72
+ if (file.filename === 'CLAUDE.md' || file.filename === 'claude.md') {
73
+ acc.claudeMd += 1;
74
+ }
75
+ if (file.filename === 'AGENTS.md' || file.filename === 'agents.md') {
76
+ acc.agentsMd += 1;
77
+ }
78
+ return acc;
79
+ }, {
80
+ claudeMd: 0,
81
+ agentsMd: 0,
82
+ total: 0,
83
+ });
84
+ return { instructionFiles, counts };
85
+ }
54
86
  function collectInstructionFiles(workingDir, boundDirs, options = {}) {
55
87
  const cwd = resolve(workingDir || process.cwd());
56
88
  const seen = new Set();
@@ -125,6 +157,7 @@ function addInstructionFile(files, seen, path, label) {
125
157
  files.push({
126
158
  path: realPath,
127
159
  label,
160
+ filename: basename(realPath),
128
161
  content: readFileSync(path, 'utf-8').trim(),
129
162
  });
130
163
  }