@openchambery/web 1.19.7-beta.9 → 1.19.8-beta.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 (203) hide show
  1. package/dist/assets/AgentAvatar-CX0a1rKE.js +1 -0
  2. package/dist/assets/AssistantView-DkXW9Fzq.js +2 -0
  3. package/dist/assets/DiagramView-C8MZY0fA.js +1 -0
  4. package/dist/assets/FadeInOnReveal-CK4asqVR.js +1 -0
  5. package/dist/assets/Icon-BYM02QyU.js +1 -0
  6. package/dist/assets/MarkdownLoadingSkeleton-DWs4Jg7b.js +3 -0
  7. package/dist/assets/MarkdownRenderer-cxYw8-8G.js +2 -0
  8. package/dist/assets/MarkdownRendererImpl-B39nwiZX.js +7 -0
  9. package/dist/assets/MarkstreamRendererImpl-CdYqdejg.js +1 -0
  10. package/dist/assets/MarkstreamRendererImpl-CsNwy1T5.css +1 -0
  11. package/dist/assets/MobileDetailNavigation-D6ri-ePj.js +1 -0
  12. package/dist/assets/MobileShareBridge-HRTN71wg.js +4 -0
  13. package/dist/assets/MobileSurface-D2yHjnZw.js +2 -0
  14. package/dist/assets/{MultiRunWindow-Ohj6UbWj.js → MultiRunWindow-CTRgfYQD.js} +1 -1
  15. package/dist/assets/OnboardingScreen-C3-wzeSX.js +1 -0
  16. package/dist/assets/OverlayScrollbar-KUpvYHdv.js +2 -0
  17. package/dist/assets/SettingsView-ImI02YSp.js +25 -0
  18. package/dist/assets/{SettingsWindow-B4lzhAbO.js → SettingsWindow-DWo2lUaN.js} +1 -1
  19. package/dist/assets/TerminalView-BseZ5dR-.js +8 -0
  20. package/dist/assets/ToolOutputDialog-BIK3cPqo.js +10 -0
  21. package/dist/assets/appThemeRegistry-4_bwYAWt.js +1 -0
  22. package/dist/assets/{button-z0GOBtWi.js → button-C0cth9tl.js} +1 -1
  23. package/dist/assets/clipboard-DJ4nQEuY.js +1 -0
  24. package/dist/assets/{collapsible-D9zJRpeB.js → collapsible-DFT9cE80.js} +1 -1
  25. package/dist/assets/context-menu-CAp74NfV.js +1 -0
  26. package/dist/assets/desktopHosts-CWxex1c3.js +1 -0
  27. package/dist/assets/desktopRelayRestore-9EbBYQ9s.js +1 -0
  28. package/dist/assets/{diagram-CoEqmReY.js → diagram-BryzMuta.js} +1 -1
  29. package/dist/assets/{dialog-x2hirWBd.js → dialog-CgbV5ljg.js} +1 -1
  30. package/dist/assets/dropdown-menu.styles-Bzo6L3FH.js +1 -0
  31. package/dist/assets/{es-C8Sqjsia.js → es-BHqnSjoZ.js} +1 -1
  32. package/dist/assets/fileQueries-BY-9OWzp.js +1 -0
  33. package/dist/assets/fileReferenceActions-BJJtX_S5.js +1 -0
  34. package/dist/assets/{fr-p-Zwm_j2.js → fr-BfrlJY9i.js} +1 -1
  35. package/dist/assets/{gitApi-CtS4kWoo.js → gitApi-m09MWmzE.js} +3 -3
  36. package/dist/assets/{input-BG3VWTIF.js → input-DFLpqjGR.js} +1 -1
  37. package/dist/assets/insertionBoundaries-B0fzEL22.js +4 -0
  38. package/dist/assets/{ja-B6REu1qP.js → ja-DP-7qt-x.js} +1 -1
  39. package/dist/assets/{ko-CJBNH9IZ.js → ko-E2B5vGQP.js} +1 -1
  40. package/dist/assets/main-BYX7_XVg.js +169 -0
  41. package/dist/assets/main-BpbNOMNf.js +2 -0
  42. package/dist/assets/markdown-worker-BuveaH3O.js +1 -0
  43. package/dist/assets/markdownTheme-CzZm3riB.js +2 -0
  44. package/dist/assets/miniChat-CFBgCVoZ.js +2 -0
  45. package/dist/assets/mobile-DcZkxRBz.js +2 -0
  46. package/dist/assets/multirun-CBqJQgS2.js +4 -0
  47. package/dist/assets/outsideFileGrants-DnplRxr-.js +1 -0
  48. package/dist/assets/path-utils-jGozTg9d.js +1 -0
  49. package/dist/assets/persistence-DdUac14I.js +3 -0
  50. package/dist/assets/{pl-B2cBI_7K.js → pl-r8X5gRM5.js} +1 -1
  51. package/dist/assets/platform-B4KKLnZ5.js +2 -0
  52. package/dist/assets/projectMeta-Ct1qHZhU.js +3 -0
  53. package/dist/assets/{pt-BR-CrCc9hmt.js → pt-BR-DcpdXCf5.js} +1 -1
  54. package/dist/assets/renderElectronMiniChatApp-DxmxYdiE.js +2 -0
  55. package/dist/assets/renderMobileApp-XXw7Fnrr.js +28 -0
  56. package/dist/assets/runtimeConfig-CcIqxMFB.js +2 -0
  57. package/dist/assets/runtimeEndpointReset-CU1HhJfC.js +147 -0
  58. package/dist/assets/sessionChangeSummary-DOq8EN_H.js +1 -0
  59. package/dist/assets/sessionLookup-CNMNR-yh.js +1 -0
  60. package/dist/assets/sortable-tabs-strip-C8_9kHqu.js +1 -0
  61. package/dist/assets/src-DasvfkEg.css +2 -0
  62. package/dist/assets/{store-CL3WP48T.js → store-BDFdnk6l.js} +2 -2
  63. package/dist/assets/terminalApi-947l7Ra3.js +1 -0
  64. package/dist/assets/textarea-Cgo_nWUU.js +1 -0
  65. package/dist/assets/toolHelpers-DEvVP9U0.js +2 -0
  66. package/dist/assets/{tooltip-CtH61mQh.js → tooltip-BifLJ8o0.js} +1 -1
  67. package/dist/assets/{uk-C8YKAz0x.js → uk-Di0vl9V-.js} +1 -1
  68. package/dist/assets/url-Pk6tQ1EZ.js +1 -0
  69. package/dist/assets/{ChatContainer-CDExXwgn.js → useAppFontEffects-CBLEH9it.js} +44 -39
  70. package/dist/assets/useAssistantUIStore-me6s63b2.js +1 -0
  71. package/dist/assets/useDesktopWindowControlsLayout-DMCmF610.js +1 -0
  72. package/dist/assets/useEffectiveDirectory-CMvw1bLA.js +1 -0
  73. package/dist/assets/useFontPreferences-BCOBlaAd.js +1 -0
  74. package/dist/assets/useMobileNavigationStore-Di9IWKfu.js +413 -0
  75. package/dist/assets/useSessionAutoCleanup-DMyMP_xI.js +1 -0
  76. package/dist/assets/useUIStore-CuaA8-Hp.js +1 -0
  77. package/dist/assets/useWorkerHighlightedLines-uNzY5rmU.js +7 -0
  78. package/dist/assets/utils-BiK3tBGM.js +1 -0
  79. package/dist/assets/{vendor-markstream-react-CwvbEg9R.js → vendor-markstream-react-C3K8YH0d.js} +4 -4
  80. package/dist/assets/{zh-CN-CCYwUnmB.js → zh-CN-coFCfeQE.js} +1 -1
  81. package/dist/assets/{zh-TW-D8Y6py08.js → zh-TW-oDRZ1-yb.js} +1 -1
  82. package/dist/index.html +9 -11
  83. package/dist/mini-chat.html +9 -11
  84. package/dist/mobile.html +15 -17
  85. package/package.json +2 -1
  86. package/server/index.js +2 -0
  87. package/server/lib/assistant-delivery-parts.js +3 -1
  88. package/server/lib/assistant-delivery-parts.test.js +11 -0
  89. package/server/lib/assistants/DOCUMENTATION.md +77 -4
  90. package/server/lib/assistants/assign.js +225 -0
  91. package/server/lib/assistants/assign.test.js +118 -0
  92. package/server/lib/assistants/bubbles.js +45 -0
  93. package/server/lib/assistants/bubbles.test.js +36 -0
  94. package/server/lib/assistants/cards.js +148 -0
  95. package/server/lib/assistants/cards.test.js +116 -0
  96. package/server/lib/assistants/contact-store.js +284 -0
  97. package/server/lib/assistants/contact-store.test.js +131 -0
  98. package/server/lib/assistants/contact-tools.js +764 -0
  99. package/server/lib/assistants/contact-tools.test.js +337 -0
  100. package/server/lib/assistants/contracts.js +20 -1
  101. package/server/lib/assistants/harness.js +575 -0
  102. package/server/lib/assistants/harness.test.js +697 -0
  103. package/server/lib/assistants/pi-tools.js +118 -0
  104. package/server/lib/assistants/pi-tools.test.js +114 -0
  105. package/server/lib/assistants/routes.js +44 -4
  106. package/server/lib/assistants/service.js +930 -17
  107. package/server/lib/assistants/service.test.js +1163 -45
  108. package/server/lib/event-stream/DOCUMENTATION.md +12 -1
  109. package/server/lib/event-stream/directory-ws-bridge.js +11 -2
  110. package/server/lib/event-stream/global-ws-bridge.js +19 -3
  111. package/server/lib/event-stream/protocol.js +11 -1
  112. package/server/lib/event-stream/reasoning-projection.js +425 -0
  113. package/server/lib/event-stream/reasoning-projection.test.js +300 -0
  114. package/server/lib/event-stream/runtime.js +10 -0
  115. package/server/lib/event-stream/runtime.test.js +53 -0
  116. package/server/lib/llm/DOCUMENTATION.md +84 -0
  117. package/server/lib/llm/catalog.js +100 -0
  118. package/server/lib/llm/catalog.test.js +78 -0
  119. package/server/lib/llm/completions.js +151 -0
  120. package/server/lib/llm/completions.test.js +127 -0
  121. package/server/lib/llm/generate.js +570 -0
  122. package/server/lib/llm/generate.test.js +459 -0
  123. package/server/lib/llm/routes.js +47 -0
  124. package/server/lib/llm/temp-directory.js +37 -0
  125. package/server/lib/notifications/APNS.md +19 -13
  126. package/server/lib/notifications/DOCUMENTATION.md +4 -3
  127. package/server/lib/notifications/runtime.js +148 -37
  128. package/server/lib/notifications/runtime.test.js +100 -0
  129. package/server/lib/opencode/DOCUMENTATION.md +2 -1
  130. package/server/lib/opencode/feature-routes-runtime.js +30 -0
  131. package/server/lib/opencode/feature-routes-runtime.test.js +20 -0
  132. package/server/lib/opencode/proxy.js +103 -9
  133. package/server/lib/opencode/proxy.test.js +177 -1
  134. package/server/lib/opencode/settings-runtime.js +2 -2
  135. package/server/lib/relay/reasoning-projection.e2e.test.ts +529 -0
  136. package/server/lib/session-goal/DOCUMENTATION.md +8 -1
  137. package/server/lib/session-goal/runtime.js +20 -0
  138. package/server/lib/session-index/DOCUMENTATION.md +3 -1
  139. package/server/lib/session-index/service.js +10 -4
  140. package/server/lib/session-index/service.test.js +37 -0
  141. package/server/lib/session-title/runtime.js +1 -0
  142. package/server/lib/session-title/runtime.test.js +11 -0
  143. package/server/lib/session-turn-pages/DOCUMENTATION.md +2 -1
  144. package/server/lib/session-turn-pages/routes.js +22 -3
  145. package/server/lib/session-turn-pages/routes.test.js +43 -0
  146. package/server/lib/transcript-cache/DOCUMENTATION.md +2 -2
  147. package/server/lib/transcript-cache/routes.js +9 -2
  148. package/server/lib/transcript-cache/routes.test.js +32 -0
  149. package/dist/assets/AgentAvatar-Di8F4QJh.js +0 -1
  150. package/dist/assets/AssistantView-DSxVAAb_.js +0 -1
  151. package/dist/assets/ContextPanel-DgwEIdqR.js +0 -285
  152. package/dist/assets/DiagramView-BKQO61y8.js +0 -1
  153. package/dist/assets/FadeInOnReveal-CV2s6YN8.js +0 -1
  154. package/dist/assets/Icon-CLpn1mIz.js +0 -1
  155. package/dist/assets/MarkdownRendererImpl-CTgmghSh.js +0 -7
  156. package/dist/assets/MarkstreamRendererImpl-BKsc-avF.js +0 -1
  157. package/dist/assets/MarkstreamRendererImpl-CxeWpDCK.css +0 -1
  158. package/dist/assets/MobileDetailNavigation-BcA6rd8Q.js +0 -1
  159. package/dist/assets/MobileShareBridge-w6T2ZrsW.js +0 -4
  160. package/dist/assets/MobileSurface-CG1n7v0h.js +0 -2
  161. package/dist/assets/OnboardingScreen-D6rtE7rj.js +0 -1
  162. package/dist/assets/OverlayScrollbar-DlNZho3q.js +0 -2
  163. package/dist/assets/SettingsView-CiF3r83e.js +0 -25
  164. package/dist/assets/TerminalView-Ctx_Jt_b.js +0 -8
  165. package/dist/assets/ToolOutputDialog-Dv9_fUYl.js +0 -10
  166. package/dist/assets/appThemeRegistry-DnwUIQB9.js +0 -2
  167. package/dist/assets/clipboard-D35XSwID.js +0 -1
  168. package/dist/assets/desktop-CfLlmafK.js +0 -1
  169. package/dist/assets/desktopHosts-C1Ns3DUu.js +0 -1
  170. package/dist/assets/desktopRelayRestore-8pvI_dSc.js +0 -1
  171. package/dist/assets/fileQueries-DGHZqZHc.js +0 -1
  172. package/dist/assets/insertionBoundaries-hK4mQj4W.js +0 -4
  173. package/dist/assets/main-BmbZFoO4.js +0 -29
  174. package/dist/assets/main-BwNhn8ml.js +0 -2
  175. package/dist/assets/markdown-worker-y3ewaqAK.js +0 -4
  176. package/dist/assets/markdownTheme-SPe4jOwX.js +0 -2
  177. package/dist/assets/miniChat-BN89siC3.js +0 -2
  178. package/dist/assets/mobile-DE9rVo5D.js +0 -2
  179. package/dist/assets/multirun-BiEuyDq3.js +0 -4
  180. package/dist/assets/persistence-w-AiX2i4.js +0 -3
  181. package/dist/assets/platform-4eCmsdol.js +0 -2
  182. package/dist/assets/projectMeta-DSLKcH1a.js +0 -3
  183. package/dist/assets/radio-HD5AB2Qf.js +0 -1
  184. package/dist/assets/renderElectronMiniChatApp-f_UxpFjv.js +0 -2
  185. package/dist/assets/renderMobileApp-BAhCai6d.js +0 -28
  186. package/dist/assets/runtime-switch-BkpTTbEi.js +0 -1
  187. package/dist/assets/runtimeConfig-qG_oNBoa.js +0 -2
  188. package/dist/assets/runtimeEndpointReset-BDeKD3EM.js +0 -4
  189. package/dist/assets/shikiHighlight-BphLZnac.js +0 -1
  190. package/dist/assets/sortable-tabs-strip-CNDgyX0B.js +0 -1
  191. package/dist/assets/src-DZt1nWf5.css +0 -2
  192. package/dist/assets/terminalApi-CGtOtc8I.js +0 -1
  193. package/dist/assets/url-KeihbE0d.js +0 -1
  194. package/dist/assets/useAppFontEffects--0dwVTSm.js +0 -7
  195. package/dist/assets/useAssistantUIStore-I0VWrB5V.js +0 -1
  196. package/dist/assets/useDesktopWindowControlsLayout-DsbeBLvv.js +0 -1
  197. package/dist/assets/useEffectiveDirectory-CHvvohcG.js +0 -1
  198. package/dist/assets/useFontPreferences-BLZ3y6mN.js +0 -1
  199. package/dist/assets/useMobileNavigationStore-peTDuC00.js +0 -412
  200. package/dist/assets/useSessionAutoCleanup-D_ZC9E5g.js +0 -1
  201. package/dist/assets/useUIStore-CNLmCoPU.js +0 -1
  202. package/dist/assets/useWorkerHighlightedLines-B1YzuMYR.js +0 -7
  203. package/dist/assets/utils-CTL16t2f.js +0 -1
@@ -0,0 +1,118 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ import {
4
+ createBashTool,
5
+ createEditTool,
6
+ createReadTool,
7
+ createWriteTool,
8
+ formatSkillsForSystemPrompt,
9
+ loadSkills,
10
+ } from '@earendil-works/pi-agent-core';
11
+ import { NodeExecutionEnv } from '@earendil-works/pi-agent-core/node';
12
+
13
+ const EXTERNAL_SKILL_ROOTS = Object.freeze(['.claude', '.agents']);
14
+
15
+ export const PI_CODING_TOOL_NAMES = Object.freeze(['read', 'write', 'edit', 'bash']);
16
+ const PI_CODING_TOOL_NAME_SET = new Set(PI_CODING_TOOL_NAMES);
17
+
18
+ export function isPiCodingToolName(name) {
19
+ return typeof name === 'string' && PI_CODING_TOOL_NAME_SET.has(name);
20
+ }
21
+
22
+ export function resolveAssistantCwd(assistant) {
23
+ for (const value of [assistant?.effectiveWorkspacePath, assistant?.workspacePath]) {
24
+ if (typeof value === 'string' && value.trim()) return value.trim();
25
+ }
26
+ return null;
27
+ }
28
+
29
+ /** Global ~/.claude/skills + ~/.agents/skills, then this project cwd (.claude then .agents). Later dirs win on name. */
30
+ export function collectAssistantSkillDirectories(cwd, { homeDir = os.homedir() } = {}) {
31
+ const dirs = [];
32
+ const seen = new Set();
33
+ const push = (dir) => {
34
+ if (typeof dir !== 'string' || !dir.trim()) return;
35
+ const resolved = path.resolve(dir);
36
+ if (seen.has(resolved)) return;
37
+ seen.add(resolved);
38
+ dirs.push(resolved);
39
+ };
40
+ const home = typeof homeDir === 'string' && homeDir.trim() ? path.resolve(homeDir.trim()) : os.homedir();
41
+ for (const root of EXTERNAL_SKILL_ROOTS) {
42
+ push(path.join(home, root, 'skills'));
43
+ }
44
+ if (typeof cwd === 'string' && cwd.trim()) {
45
+ const project = path.resolve(cwd.trim());
46
+ for (const root of EXTERNAL_SKILL_ROOTS) {
47
+ push(path.join(project, root, 'skills'));
48
+ }
49
+ }
50
+ return dirs;
51
+ }
52
+
53
+ export function mergeSkillsByName(skills) {
54
+ const map = new Map();
55
+ for (const skill of Array.isArray(skills) ? skills : []) {
56
+ const name = typeof skill?.name === 'string' ? skill.name.trim() : '';
57
+ if (!name) continue;
58
+ map.set(name, skill);
59
+ }
60
+ return [...map.values()];
61
+ }
62
+
63
+ const bindHarnessTool = (tool, context) => ({
64
+ name: tool.name,
65
+ label: tool.label,
66
+ description: tool.description,
67
+ parameters: tool.parameters,
68
+ execute: (toolCallId, params, signal, onUpdate) => (
69
+ tool.execute(toolCallId, params, signal, onUpdate, context)
70
+ ),
71
+ });
72
+
73
+ export function formatPiCodingPrompt({ cwd, skillsPrompt = '' } = {}) {
74
+ if (typeof cwd !== 'string' || !cwd.trim()) return '';
75
+ return [
76
+ `Working directory: ${cwd}`,
77
+ 'You have pi coding tools in this directory: read, write, edit, bash.',
78
+ 'This is your working directory. Ignore any other cwd from the environment, including temporary generator workspaces under /var/folders or os.tmpdir.',
79
+ 'When the user asks pwd, the current directory, or to look at files, call bash/read yourself. Never say you have no terminal or cannot read or write files.',
80
+ 'Skills are merged from ~/.claude/skills, ~/.agents/skills, and this project\'s .claude/skills plus .agents/skills (project wins on name). Read a skill file when the task matches its description.',
81
+ 'Call these tools with the same fenced JSON as other tools:',
82
+ '```openchamber-tool',
83
+ '{"name":"bash","arguments":{"command":"pwd"}}',
84
+ '```',
85
+ '```openchamber-tool',
86
+ '{"name":"read","arguments":{"path":".agents/skills/example/SKILL.md"}}',
87
+ '```',
88
+ typeof skillsPrompt === 'string' && skillsPrompt.trim() ? skillsPrompt.trim() : '',
89
+ ].filter(Boolean).join('\n');
90
+ }
91
+
92
+ export async function createPiCodingRuntime(cwd, { homeDir = os.homedir() } = {}) {
93
+ if (typeof cwd !== 'string' || !cwd.trim()) return null;
94
+ const resolved = cwd.trim();
95
+ const env = new NodeExecutionEnv({ cwd: resolved });
96
+ const context = { env };
97
+ const tools = [
98
+ bindHarnessTool(createReadTool(), context),
99
+ bindHarnessTool(createWriteTool(), context),
100
+ bindHarnessTool(createEditTool(), context),
101
+ bindHarnessTool(createBashTool(), context),
102
+ ];
103
+ let skillsPrompt = '';
104
+ try {
105
+ const loaded = await loadSkills(env, collectAssistantSkillDirectories(resolved, { homeDir }));
106
+ skillsPrompt = formatSkillsForSystemPrompt(mergeSkillsByName(loaded?.skills));
107
+ } catch {
108
+ skillsPrompt = '';
109
+ }
110
+ return {
111
+ cwd: resolved,
112
+ tools,
113
+ skillsPrompt,
114
+ close: async () => {
115
+ await env.cleanup();
116
+ },
117
+ };
118
+ }
@@ -0,0 +1,114 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { afterEach, describe, expect, it } from 'vitest';
5
+ import {
6
+ collectAssistantSkillDirectories,
7
+ createPiCodingRuntime,
8
+ mergeSkillsByName,
9
+ PI_CODING_TOOL_NAMES,
10
+ resolveAssistantCwd,
11
+ } from './pi-tools.js';
12
+
13
+ const temps = [];
14
+ const temp = () => {
15
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'oc-pi-tools-'));
16
+ temps.push(dir);
17
+ return dir;
18
+ };
19
+ const writeSkill = (root, name, description = `${name} skill`) => {
20
+ const dir = path.join(root, name);
21
+ fs.mkdirSync(dir, { recursive: true });
22
+ fs.writeFileSync(path.join(dir, 'SKILL.md'), `---\nname: ${name}\ndescription: ${description}\n---\nInstructions for ${name}\n`);
23
+ };
24
+
25
+ afterEach(() => {
26
+ while (temps.length > 0) {
27
+ fs.rmSync(temps.pop(), { recursive: true, force: true });
28
+ }
29
+ });
30
+
31
+ describe('resolveAssistantCwd', () => {
32
+ it('prefers effectiveWorkspacePath over workspacePath', () => {
33
+ expect(resolveAssistantCwd({
34
+ effectiveWorkspacePath: '/effective',
35
+ workspacePath: '/workspace',
36
+ })).toBe('/effective');
37
+ expect(resolveAssistantCwd({ workspacePath: '/workspace' })).toBe('/workspace');
38
+ expect(resolveAssistantCwd({ defaultPrompt: 'x' })).toBeNull();
39
+ });
40
+ });
41
+
42
+ describe('collectAssistantSkillDirectories', () => {
43
+ it('lists global ~/.claude and ~/.agents then project .claude and .agents', () => {
44
+ const home = temp();
45
+ const cwd = temp();
46
+ expect(collectAssistantSkillDirectories(cwd, { homeDir: home })).toEqual([
47
+ path.resolve(home, '.claude', 'skills'),
48
+ path.resolve(home, '.agents', 'skills'),
49
+ path.resolve(cwd, '.claude', 'skills'),
50
+ path.resolve(cwd, '.agents', 'skills'),
51
+ ]);
52
+ });
53
+
54
+ it('does not invent skill roots on intermediate parent folders', () => {
55
+ const home = temp();
56
+ const root = temp();
57
+ const nested = path.join(root, 'apps', 'web');
58
+ fs.mkdirSync(nested, { recursive: true });
59
+ expect(collectAssistantSkillDirectories(nested, { homeDir: home })).toEqual([
60
+ path.resolve(home, '.claude', 'skills'),
61
+ path.resolve(home, '.agents', 'skills'),
62
+ path.resolve(nested, '.claude', 'skills'),
63
+ path.resolve(nested, '.agents', 'skills'),
64
+ ]);
65
+ });
66
+ });
67
+
68
+ describe('mergeSkillsByName', () => {
69
+ it('keeps the last skill with the same name', () => {
70
+ expect(mergeSkillsByName([
71
+ { name: 'shared', filePath: '/global' },
72
+ { name: 'shared', filePath: '/project' },
73
+ { name: 'other', filePath: '/other' },
74
+ ])).toEqual([
75
+ { name: 'shared', filePath: '/project' },
76
+ { name: 'other', filePath: '/other' },
77
+ ]);
78
+ });
79
+ });
80
+
81
+ describe('createPiCodingRuntime', () => {
82
+ it('installs read/write/edit/bash and merges global plus project skills', async () => {
83
+ const home = temp();
84
+ const cwd = temp();
85
+ writeSkill(path.join(home, '.claude', 'skills'), 'claude-global', 'Claude global');
86
+ writeSkill(path.join(home, '.agents', 'skills'), 'agents-global', 'Agents global');
87
+ writeSkill(path.join(cwd, '.claude', 'skills'), 'claude-project', 'Claude project');
88
+ writeSkill(path.join(cwd, '.agents', 'skills'), 'project-skill', 'Project skill');
89
+ writeSkill(path.join(home, '.agents', 'skills'), 'shared', 'Global shared');
90
+ writeSkill(path.join(cwd, '.agents', 'skills'), 'shared', 'Project shared');
91
+
92
+ const runtime = await createPiCodingRuntime(cwd, { homeDir: home });
93
+ expect(runtime.tools.map((tool) => tool.name)).toEqual([...PI_CODING_TOOL_NAMES]);
94
+ expect(runtime.skillsPrompt).toContain('claude-global');
95
+ expect(runtime.skillsPrompt).toContain('agents-global');
96
+ expect(runtime.skillsPrompt).toContain('claude-project');
97
+ expect(runtime.skillsPrompt).toContain('project-skill');
98
+ expect(runtime.skillsPrompt).toContain('Project shared');
99
+ expect(runtime.skillsPrompt).not.toContain('Global shared');
100
+ await runtime.close();
101
+ });
102
+
103
+ it('runs bash pwd in the workspace', async () => {
104
+ const cwd = temp();
105
+ const home = temp();
106
+ const runtime = await createPiCodingRuntime(cwd, { homeDir: home });
107
+ const bash = runtime.tools.find((tool) => tool.name === 'bash');
108
+ const result = await bash.execute('call_pwd', { command: 'pwd' });
109
+ const text = result.content.map((part) => part.text).join('');
110
+ const resolved = fs.realpathSync(cwd);
111
+ expect(text.includes(cwd) || text.includes(resolved)).toBe(true);
112
+ await runtime.close();
113
+ });
114
+ });
@@ -1,17 +1,57 @@
1
1
  import { AssistantError, createAssistantsService } from './service.js';
2
+ import { createChatCompletion } from '../llm/completions.js';
3
+ import { ensureLlmTempDirectory } from '../llm/temp-directory.js';
4
+ import { setAssignedSessionSettleHandler } from '../session-goal/runtime.js';
5
+ import { createOpencodeClient } from '@opencode-ai/sdk/v2';
2
6
 
3
- const respond = (res, work, success = 200) => Promise.resolve().then(work).then((body) => res.status(success).json(body)).catch((error) => { const code = error instanceof AssistantError ? error.code : 'internal_error'; const status = code === 'not_found' ? 404 : ['revision_conflict', 'idempotency_conflict'].includes(code) ? 409 : code === 'assistant_disabled' ? 403 : code === 'upstream_error' ? 502 : 400; res.status(status).json({ ok: false, error: code }); });
7
+ const respond = (res, work, success = 200) => Promise.resolve().then(work).then((body) => res.status(success).json(body)).catch((error) => { const code = error instanceof AssistantError ? error.code : error?.code || 'internal_error'; const status = code === 'not_found' ? 404 : ['revision_conflict', 'idempotency_conflict'].includes(code) ? 409 : code === 'assistant_disabled' ? 403 : code === 'no_provider' ? 400 : code === 'upstream_error' ? 502 : 400; res.status(status).json({ ok: false, error: code, message: typeof error?.message === 'string' && error.message.trim() ? error.message : code }); });
4
8
  const gone = (_req, res) => res.status(410).json({ ok: false, error: 'assistant_topics_retired' });
5
9
 
6
10
  export const registerAssistantRoutes = (app, dependencies) => {
7
- const service = createAssistantsService({ dbPath: dependencies.dbPath, dataDir: dependencies.openchamberDataDir, buildOpenCodeUrl: dependencies.buildOpenCodeUrl, getOpenCodeAuthHeaders: dependencies.getOpenCodeAuthHeaders, getServerId: dependencies.getServerId, getAllowedRoots: dependencies.getAllowedRoots, globalEventHub: dependencies.globalEventHub, onRevisionTip: dependencies.onRevisionTip });
11
+ const boundCompletion = (input) => createChatCompletion({
12
+ ...input,
13
+ buildOpenCodeUrl: dependencies.buildOpenCodeUrl,
14
+ getOpenCodeAuthHeaders: dependencies.getOpenCodeAuthHeaders,
15
+ clientFactory: () => createOpencodeClient({
16
+ baseUrl: dependencies.buildOpenCodeUrl('/', '').replace(/\/$/, ''),
17
+ headers: dependencies.getOpenCodeAuthHeaders(),
18
+ }),
19
+ ensureTempDirectory: ensureLlmTempDirectory,
20
+ // Throwaway generate may subscribe for real token deltas (contact harness).
21
+ globalEventHub: input?.globalEventHub ?? dependencies.globalEventHub,
22
+ });
23
+ const service = createAssistantsService({
24
+ dbPath: dependencies.dbPath,
25
+ dataDir: dependencies.openchamberDataDir,
26
+ buildOpenCodeUrl: dependencies.buildOpenCodeUrl,
27
+ getOpenCodeAuthHeaders: dependencies.getOpenCodeAuthHeaders,
28
+ getServerId: dependencies.getServerId,
29
+ getAllowedRoots: dependencies.getAllowedRoots,
30
+ listProjects: dependencies.listProjects,
31
+ listScheduledTasks: dependencies.listScheduledTasks,
32
+ sessionIndexService: dependencies.sessionIndexService,
33
+ upsertScheduledTask: dependencies.upsertScheduledTask,
34
+ syncScheduledTaskProject: dependencies.syncScheduledTaskProject,
35
+ globalEventHub: dependencies.globalEventHub,
36
+ onRevisionTip: dependencies.onRevisionTip,
37
+ onContactTurnEvent: dependencies.onContactTurnEvent,
38
+ onContactTurnComplete: dependencies.onContactTurnComplete,
39
+ createChatCompletion: boundCompletion,
40
+ });
41
+ setAssignedSessionSettleHandler(({ sessionId, status }) => service.reportAssignedSessionSettle(sessionId, status));
8
42
  app.use('/api/openchamber/assistants', (req, res, next) => Promise.resolve(dependencies.refreshAllowedRoots?.()).then(next).catch((error) => respond(res, () => { throw error; })));
9
43
  app.get('/api/openchamber/assistants/capability', (_req, res) => respond(res, () => service.capability()));
10
44
  app.get('/api/openchamber/assistants', (_req, res) => respond(res, () => service.snapshot())); app.get('/api/openchamber/assistants/snapshot', (_req, res) => respond(res, () => service.snapshot())); app.put('/api/openchamber/assistants/settings', (req, res) => respond(res, () => service.setEnabled(req.body)));
11
45
  app.post('/api/openchamber/assistants', (req, res) => respond(res, () => service.createAssistant(req.body), 201)); app.patch('/api/openchamber/assistants/:assistantID', (req, res) => respond(res, () => service.updateAssistant(req.params.assistantID, req.body))); app.delete('/api/openchamber/assistants/:assistantID', (req, res) => respond(res, () => service.removeAssistant(req.params.assistantID, req.body?.expectedRevision)));
12
46
  app.post('/api/openchamber/assistants/:assistantID/session/ensure', (req, res) => respond(res, () => service.ensure(req.params.assistantID))); app.post('/api/openchamber/assistants/:assistantID/session/new', (req, res) => respond(res, () => service.createNew(req.params.assistantID))); app.post('/api/openchamber/assistants/:assistantID/session/compact', (req, res) => respond(res, () => service.compact(req.params.assistantID, req.body))); app.post('/api/openchamber/assistants/:assistantID/session/abort', (req, res) => respond(res, () => service.abort(req.params.assistantID, req.body)));
13
47
  app.get('/api/openchamber/assistants/:assistantID/messages', (req, res) => respond(res, () => service.historicalMessages(req.params.assistantID, req.query)));
14
- app.post('/api/openchamber/assistants/:assistantID/messages', (req, res) => respond(res, () => service.send(req.params.assistantID, req.body))); app.post('/api/openchamber/assistants/:assistantID/share', (req, res) => respond(res, () => service.share(req.params.assistantID, req.body), 202)); app.get('/api/openchamber/assistants/share-operations/:operationID', (req, res) => respond(res, () => { const operation = service.shareOperation(req.params.operationID); if (!operation) throw new AssistantError('not_found'); return operation; }));
48
+ app.get('/api/openchamber/assistants/:assistantID/contact/messages', (req, res) => respond(res, () => service.contactMessages(req.params.assistantID, req.query)));
49
+ app.post('/api/openchamber/assistants/:assistantID/contact/reset', (req, res) => respond(res, () => service.resetContact(req.params.assistantID)));
50
+ app.post('/api/openchamber/assistants/:assistantID/contact/cards', (req, res) => respond(res, () => service.appendContactCard(req.params.assistantID, req.body), 201));
51
+ app.post('/api/openchamber/assistants/:assistantID/contact/dm', (req, res) => respond(res, () => service.deliverPeerMessage(req.params.assistantID, req.body), 201));
52
+ app.get('/api/openchamber/assistants/:assistantID/scheduled-tasks', (req, res) => respond(res, () => service.listAssistantScheduledTasks(req.params.assistantID)));
53
+ // Contact send admits the user message at 202; assistant bubbles stream on SSE.
54
+ app.post('/api/openchamber/assistants/:assistantID/messages', (req, res) => respond(res, () => service.send(req.params.assistantID, req.body), 202)); app.post('/api/openchamber/assistants/:assistantID/share', (req, res) => respond(res, () => service.share(req.params.assistantID, req.body), 202)); app.get('/api/openchamber/assistants/share-operations/:operationID', (req, res) => respond(res, () => { const operation = service.shareOperation(req.params.operationID); if (!operation) throw new AssistantError('not_found'); return operation; }));
15
55
  app.use('/api/openchamber/assistants/topics', gone); app.use('/api/openchamber/assistants/:assistantID/topics', gone);
16
- return { service, close: () => service.close() };
56
+ return { service, close: () => { setAssignedSessionSettleHandler(null); service.close(); } };
17
57
  };