pikiloop 0.4.0

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 (154) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +353 -0
  3. package/README.v2.md +287 -0
  4. package/README.zh-CN.md +352 -0
  5. package/dashboard/dist/assets/AgentTab-UZPIhlkr.js +1 -0
  6. package/dashboard/dist/assets/DirBrowser-Ckcmi-Pi.js +1 -0
  7. package/dashboard/dist/assets/ExtensionsTab-KZhEDrdu.js +1 -0
  8. package/dashboard/dist/assets/IMAccessTab-Bd_IY1GQ.js +1 -0
  9. package/dashboard/dist/assets/Modal-CTeL0y7P.js +1 -0
  10. package/dashboard/dist/assets/Modals-axftHasy.js +1 -0
  11. package/dashboard/dist/assets/Select-C8tOdPhe.js +1 -0
  12. package/dashboard/dist/assets/SessionPanel-C1geSRxw.js +1 -0
  13. package/dashboard/dist/assets/SystemTab-DBDkaPiO.js +1 -0
  14. package/dashboard/dist/assets/anthropic-BAdojD7P.ico +0 -0
  15. package/dashboard/dist/assets/codex-DYadqqp0.png +0 -0
  16. package/dashboard/dist/assets/deepseek-BeYNZEk0.ico +0 -0
  17. package/dashboard/dist/assets/doubao-DloFDuFR.png +0 -0
  18. package/dashboard/dist/assets/feishu-C4OMrjCW.ico +0 -0
  19. package/dashboard/dist/assets/gemini-BYkEpiWr.svg +1 -0
  20. package/dashboard/dist/assets/hermes-BAarh-tH.png +0 -0
  21. package/dashboard/dist/assets/index-CpM4CqZJ.js +23 -0
  22. package/dashboard/dist/assets/index-DXSohzrE.js +3 -0
  23. package/dashboard/dist/assets/index-reSbuley.css +1 -0
  24. package/dashboard/dist/assets/markdown-DxQYQFeH.js +29 -0
  25. package/dashboard/dist/assets/minimax-PuEGTfrF.ico +0 -0
  26. package/dashboard/dist/assets/mlx-DhWwjtMw.png +0 -0
  27. package/dashboard/dist/assets/ollama-Bt9O-2K_.png +0 -0
  28. package/dashboard/dist/assets/openrouter-CsJ_bD5Q.ico +0 -0
  29. package/dashboard/dist/assets/playwright-BldPFZgC.ico +0 -0
  30. package/dashboard/dist/assets/qwen-xykkX0_y.png +0 -0
  31. package/dashboard/dist/assets/react-vendor-C7Sl8SE7.js +9 -0
  32. package/dashboard/dist/assets/router-DHISdpPk.js +3 -0
  33. package/dashboard/dist/assets/shared-BIP_4k4I.js +1 -0
  34. package/dashboard/dist/favicon.svg +28 -0
  35. package/dashboard/dist/index.html +17 -0
  36. package/dist/agent/acp-client.js +261 -0
  37. package/dist/agent/auto-update.js +432 -0
  38. package/dist/agent/await-resume.js +50 -0
  39. package/dist/agent/cli/auth.js +325 -0
  40. package/dist/agent/cli/catalog.js +40 -0
  41. package/dist/agent/cli/detector.js +136 -0
  42. package/dist/agent/cli/index.js +7 -0
  43. package/dist/agent/cli/registry.js +33 -0
  44. package/dist/agent/driver.js +39 -0
  45. package/dist/agent/drivers/claude-tui.js +2297 -0
  46. package/dist/agent/drivers/claude.js +2689 -0
  47. package/dist/agent/drivers/codex.js +2210 -0
  48. package/dist/agent/drivers/gemini.js +1059 -0
  49. package/dist/agent/drivers/hermes.js +795 -0
  50. package/dist/agent/goal.js +274 -0
  51. package/dist/agent/handover.js +130 -0
  52. package/dist/agent/images.js +355 -0
  53. package/dist/agent/index.js +50 -0
  54. package/dist/agent/mcp/bridge.js +791 -0
  55. package/dist/agent/mcp/extensions.js +637 -0
  56. package/dist/agent/mcp/oauth.js +353 -0
  57. package/dist/agent/mcp/registry.js +119 -0
  58. package/dist/agent/mcp/session-server.js +229 -0
  59. package/dist/agent/mcp/tools/ask-user.js +113 -0
  60. package/dist/agent/mcp/tools/await-resume.js +77 -0
  61. package/dist/agent/mcp/tools/goal.js +144 -0
  62. package/dist/agent/mcp/tools/types.js +12 -0
  63. package/dist/agent/mcp/tools/workspace.js +212 -0
  64. package/dist/agent/npm.js +31 -0
  65. package/dist/agent/session.js +1206 -0
  66. package/dist/agent/skill-installer.js +160 -0
  67. package/dist/agent/skills.js +257 -0
  68. package/dist/agent/stream.js +743 -0
  69. package/dist/agent/types.js +13 -0
  70. package/dist/agent/utils.js +687 -0
  71. package/dist/bot/bot.js +2499 -0
  72. package/dist/bot/command-ui.js +633 -0
  73. package/dist/bot/commands.js +513 -0
  74. package/dist/bot/headless-bot.js +36 -0
  75. package/dist/bot/host.js +192 -0
  76. package/dist/bot/human-loop.js +168 -0
  77. package/dist/bot/menu.js +48 -0
  78. package/dist/bot/orchestration.js +79 -0
  79. package/dist/bot/render-shared.js +309 -0
  80. package/dist/bot/session-hub.js +361 -0
  81. package/dist/bot/session-status.js +55 -0
  82. package/dist/bot/streaming.js +309 -0
  83. package/dist/browser-profile.js +579 -0
  84. package/dist/browser-supervisor.js +249 -0
  85. package/dist/catalog/cli-tools.js +421 -0
  86. package/dist/catalog/index.js +21 -0
  87. package/dist/catalog/local-models.js +94 -0
  88. package/dist/catalog/mcp-servers.js +315 -0
  89. package/dist/catalog/skill-repos.js +173 -0
  90. package/dist/channels/base.js +55 -0
  91. package/dist/channels/dingtalk/bot.js +549 -0
  92. package/dist/channels/dingtalk/channel.js +268 -0
  93. package/dist/channels/discord/bot.js +552 -0
  94. package/dist/channels/discord/channel.js +245 -0
  95. package/dist/channels/feishu/bot.js +1275 -0
  96. package/dist/channels/feishu/channel.js +911 -0
  97. package/dist/channels/feishu/markdown.js +91 -0
  98. package/dist/channels/feishu/render.js +619 -0
  99. package/dist/channels/health.js +109 -0
  100. package/dist/channels/slack/bot.js +554 -0
  101. package/dist/channels/slack/channel.js +283 -0
  102. package/dist/channels/states.js +6 -0
  103. package/dist/channels/telegram/bot.js +1310 -0
  104. package/dist/channels/telegram/channel.js +820 -0
  105. package/dist/channels/telegram/directory.js +111 -0
  106. package/dist/channels/telegram/live-preview.js +220 -0
  107. package/dist/channels/telegram/render.js +384 -0
  108. package/dist/channels/wecom/bot.js +558 -0
  109. package/dist/channels/wecom/channel.js +479 -0
  110. package/dist/channels/weixin/api.js +520 -0
  111. package/dist/channels/weixin/bot.js +1000 -0
  112. package/dist/channels/weixin/channel.js +222 -0
  113. package/dist/cli/autostart.js +262 -0
  114. package/dist/cli/channel-supervisor.js +313 -0
  115. package/dist/cli/channels.js +54 -0
  116. package/dist/cli/main.js +726 -0
  117. package/dist/cli/onboarding.js +227 -0
  118. package/dist/cli/run.js +308 -0
  119. package/dist/cli/setup-wizard.js +235 -0
  120. package/dist/core/config/runtime-config.js +201 -0
  121. package/dist/core/config/user-config.js +510 -0
  122. package/dist/core/config/validation.js +521 -0
  123. package/dist/core/constants.js +400 -0
  124. package/dist/core/git.js +145 -0
  125. package/dist/core/legacy-compat.js +60 -0
  126. package/dist/core/logging.js +101 -0
  127. package/dist/core/platform.js +59 -0
  128. package/dist/core/process-control.js +315 -0
  129. package/dist/core/secrets/index.js +42 -0
  130. package/dist/core/secrets/inline-seal.js +60 -0
  131. package/dist/core/secrets/ref.js +33 -0
  132. package/dist/core/secrets/resolver.js +65 -0
  133. package/dist/core/secrets/store.js +63 -0
  134. package/dist/core/utils.js +233 -0
  135. package/dist/core/version.js +15 -0
  136. package/dist/dashboard/platform.js +219 -0
  137. package/dist/dashboard/routes/agents.js +450 -0
  138. package/dist/dashboard/routes/cli.js +174 -0
  139. package/dist/dashboard/routes/config.js +523 -0
  140. package/dist/dashboard/routes/extensions.js +745 -0
  141. package/dist/dashboard/routes/local-models.js +290 -0
  142. package/dist/dashboard/routes/models.js +324 -0
  143. package/dist/dashboard/routes/sessions.js +838 -0
  144. package/dist/dashboard/runtime.js +410 -0
  145. package/dist/dashboard/server.js +237 -0
  146. package/dist/dashboard/session-control.js +347 -0
  147. package/dist/model/catalog.js +104 -0
  148. package/dist/model/index.js +20 -0
  149. package/dist/model/injector.js +272 -0
  150. package/dist/model/provider-models.js +112 -0
  151. package/dist/model/store.js +212 -0
  152. package/dist/model/types.js +13 -0
  153. package/dist/model/validation.js +203 -0
  154. package/package.json +82 -0
@@ -0,0 +1,229 @@
1
+ /**
2
+ * mcp-session-server.ts — MCP server process for pikiloop session bridge.
3
+ *
4
+ * Spawned by the agent CLI (claude/codex/gemini) via --mcp-config or codex mcp add.
5
+ * Communicates with the agent over stdio using the MCP protocol (JSON-RPC 2.0).
6
+ *
7
+ * Supports two stdio transports (auto-detected from first byte):
8
+ * - Content-Length framing (Claude, Gemini — standard MCP/LSP)
9
+ * - Newline-delimited JSON (Codex)
10
+ *
11
+ * Context is injected via environment variables:
12
+ * MCP_WORKSPACE_PATH — absolute path to the session workspace
13
+ * MCP_STAGED_FILES — JSON array of staged file relative paths
14
+ * MCP_CALLBACK_URL — HTTP URL for the pikiloop callback server
15
+ *
16
+ * Tools are defined in src/tools/ — each module exports definitions + handlers.
17
+ */
18
+ import path from 'node:path';
19
+ import { createRetainedLogSink, writeScopedLog } from '../../core/logging.js';
20
+ import { workspaceTools } from './tools/workspace.js';
21
+ import { goalTools } from './tools/goal.js';
22
+ import { awaitResumeTools } from './tools/await-resume.js';
23
+ import { askUserTools } from './tools/ask-user.js';
24
+ // ---------------------------------------------------------------------------
25
+ // Logging — writes to stderr + file so it doesn't interfere with stdio MCP transport
26
+ // ---------------------------------------------------------------------------
27
+ const _logSink = (() => {
28
+ try {
29
+ const ws = process.env.MCP_WORKSPACE_PATH || '';
30
+ if (!ws)
31
+ return null;
32
+ const dir = path.dirname(ws);
33
+ return createRetainedLogSink(path.join(dir, 'mcp-server.log'));
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ })();
39
+ function log(msg, level = 'debug') {
40
+ if (!writeScopedLog('mcp-server', msg, { level, stream: 'stderr' }))
41
+ return;
42
+ _logSink?.(`[mcp-server ${new Date().toTimeString().slice(0, 8)}] ${msg}\n`);
43
+ }
44
+ function summarizeArgs(args, max = 200) {
45
+ let text = '';
46
+ try {
47
+ text = JSON.stringify(args);
48
+ }
49
+ catch {
50
+ text = String(args);
51
+ }
52
+ if (!text)
53
+ return '{}';
54
+ return text.length <= max ? text : `${text.slice(0, Math.max(0, max - 3)).trimEnd()}...`;
55
+ }
56
+ // ---------------------------------------------------------------------------
57
+ // Context from environment
58
+ // ---------------------------------------------------------------------------
59
+ const ctx = {
60
+ workspace: process.env.MCP_WORKSPACE_PATH || '',
61
+ workdir: process.env.MCP_WORKDIR || undefined,
62
+ stagedFiles: (() => {
63
+ try {
64
+ return JSON.parse(process.env.MCP_STAGED_FILES || '[]');
65
+ }
66
+ catch {
67
+ return [];
68
+ }
69
+ })(),
70
+ callbackUrl: process.env.MCP_CALLBACK_URL || '',
71
+ };
72
+ log(`started workspace=${ctx.workspace} stagedFiles=${ctx.stagedFiles.length} callbackUrl=${ctx.callbackUrl ? 'set' : 'MISSING'}`);
73
+ // ---------------------------------------------------------------------------
74
+ // Tool registry — collect all tool modules
75
+ // ---------------------------------------------------------------------------
76
+ // `MCP_TOOLS_AVAILABLE` lists tool families the bridge has wired up. Codex
77
+ // has a native `/goal` implementation and native user-input, so it skips
78
+ // `goalTools` and never receives `ask-user` via the bridge.
79
+ const AVAILABLE = new Set((process.env.MCP_TOOLS_AVAILABLE || '').split(',').map(s => s.trim()).filter(Boolean));
80
+ const IS_CODEX = process.env.MCP_AGENT === 'codex';
81
+ const TOOL_MODULES = [
82
+ ...(AVAILABLE.has('workspace') ? [workspaceTools] : []),
83
+ ...(IS_CODEX ? [] : [goalTools]),
84
+ // Codex parks/resumes via its own native goal machinery; the pikiloop
85
+ // awaiting marker is for the `claude -p`-style drivers whose turn process
86
+ // exits at `result`.
87
+ ...(IS_CODEX ? [] : [awaitResumeTools]),
88
+ ...(AVAILABLE.has('ask-user') ? [askUserTools] : []),
89
+ ];
90
+ const ALL_TOOLS = TOOL_MODULES.flatMap(m => m.tools);
91
+ /** Lookup: tool name → module that handles it. */
92
+ const TOOL_HANDLERS = new Map();
93
+ for (const mod of TOOL_MODULES) {
94
+ for (const t of mod.tools) {
95
+ TOOL_HANDLERS.set(t.name, mod);
96
+ }
97
+ }
98
+ // ---------------------------------------------------------------------------
99
+ // MCP protocol — auto-detect transport format
100
+ // ---------------------------------------------------------------------------
101
+ /** 'framed' = Content-Length (Claude/Gemini), 'ndjson' = newline-delimited (Codex) */
102
+ let transport = null;
103
+ function send(msg) {
104
+ const body = JSON.stringify(msg);
105
+ if (transport === 'ndjson') {
106
+ process.stdout.write(body + '\n');
107
+ }
108
+ else {
109
+ process.stdout.write(`Content-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`);
110
+ }
111
+ }
112
+ function respond(id, result) {
113
+ send({ jsonrpc: '2.0', id, result });
114
+ }
115
+ function respondError(id, code, message) {
116
+ send({ jsonrpc: '2.0', id, error: { code, message } });
117
+ }
118
+ // ---------------------------------------------------------------------------
119
+ // Stdio reader — auto-detecting Content-Length framed vs NDJSON
120
+ // ---------------------------------------------------------------------------
121
+ let buffer = '';
122
+ function processFramed() {
123
+ while (true) {
124
+ const headerEnd = buffer.indexOf('\r\n\r\n');
125
+ if (headerEnd < 0)
126
+ break;
127
+ const header = buffer.slice(0, headerEnd);
128
+ const match = header.match(/Content-Length:\s*(\d+)/i);
129
+ if (!match) {
130
+ buffer = buffer.slice(headerEnd + 4);
131
+ continue;
132
+ }
133
+ const len = parseInt(match[1], 10);
134
+ const bodyStart = headerEnd + 4;
135
+ if (buffer.length < bodyStart + len)
136
+ break;
137
+ const body = buffer.slice(bodyStart, bodyStart + len);
138
+ buffer = buffer.slice(bodyStart + len);
139
+ try {
140
+ handleMessage(JSON.parse(body));
141
+ }
142
+ catch { /* ignore parse errors */ }
143
+ }
144
+ }
145
+ function processNdjson() {
146
+ while (true) {
147
+ const newlineIdx = buffer.indexOf('\n');
148
+ if (newlineIdx < 0)
149
+ break;
150
+ const line = buffer.slice(0, newlineIdx).trim();
151
+ buffer = buffer.slice(newlineIdx + 1);
152
+ if (!line)
153
+ continue;
154
+ try {
155
+ handleMessage(JSON.parse(line));
156
+ }
157
+ catch { /* ignore parse errors */ }
158
+ }
159
+ }
160
+ function processBuffer() {
161
+ if (transport === null) {
162
+ const trimmed = buffer.trimStart();
163
+ if (!trimmed)
164
+ return;
165
+ transport = trimmed[0] === '{' ? 'ndjson' : 'framed';
166
+ }
167
+ if (transport === 'ndjson')
168
+ processNdjson();
169
+ else
170
+ processFramed();
171
+ }
172
+ process.stdin.setEncoding('utf-8');
173
+ process.stdin.on('data', (chunk) => {
174
+ buffer += chunk;
175
+ processBuffer();
176
+ });
177
+ process.stdin.on('end', () => process.exit(0));
178
+ // ---------------------------------------------------------------------------
179
+ // Message dispatcher
180
+ // ---------------------------------------------------------------------------
181
+ function handleMessage(msg) {
182
+ const { id, method, params } = msg;
183
+ switch (method) {
184
+ case 'initialize':
185
+ log(`initialize protocolVersion=${params?.protocolVersion || '?'}`);
186
+ respond(id, {
187
+ protocolVersion: params?.protocolVersion || '2024-11-05',
188
+ capabilities: { tools: {} },
189
+ serverInfo: { name: 'pikiloop-session', version: '1.0.0' },
190
+ });
191
+ break;
192
+ case 'notifications/initialized':
193
+ log('initialized notification received');
194
+ break;
195
+ case 'tools/list':
196
+ log(`tools/list → ${ALL_TOOLS.length} tools: ${ALL_TOOLS.map(t => t.name).join(', ')}`);
197
+ respond(id, { tools: ALL_TOOLS });
198
+ break;
199
+ case 'tools/call': {
200
+ const name = params?.name;
201
+ const args = params?.arguments || {};
202
+ const mod = TOOL_HANDLERS.get(name);
203
+ if (!mod) {
204
+ log(`tools/call UNKNOWN tool="${name}"`, 'warn');
205
+ respondError(id, -32601, `Unknown tool: ${name}`);
206
+ break;
207
+ }
208
+ const argsSummary = summarizeArgs(args);
209
+ log(`tools/call tool="${name}" args=${argsSummary}`);
210
+ const callStart = Date.now();
211
+ void Promise.resolve(mod.handle(name, args, ctx)).then(result => {
212
+ const elapsed = Date.now() - callStart;
213
+ const text = result.content?.[0]?.text || '';
214
+ log(`tools/call tool="${name}" ${result.isError ? 'ERROR' : 'OK'} ${elapsed}ms args=${argsSummary} result=${text.slice(0, 150)}`);
215
+ respond(id, result);
216
+ }, err => {
217
+ const elapsed = Date.now() - callStart;
218
+ log(`tools/call tool="${name}" EXCEPTION ${elapsed}ms args=${argsSummary} error=${err?.message || err}`, 'warn');
219
+ respond(id, { content: [{ type: 'text', text: `Tool error: ${err?.message || err}` }], isError: true });
220
+ });
221
+ break;
222
+ }
223
+ default:
224
+ if (id !== undefined) {
225
+ log(`unknown method="${method}"`, 'warn');
226
+ respondError(id, -32601, `Method not found: ${method}`);
227
+ }
228
+ }
229
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * tools/ask-user.ts — im_ask_user: block the current turn and route a question
3
+ * to the user via IM or dashboard. Used in place of Claude's native
4
+ * `AskUserQuestion`, which is unusable in `-p` mode.
5
+ */
6
+ import http from 'node:http';
7
+ import { toolResult, toolLog } from './types.js';
8
+ const tools = [
9
+ {
10
+ name: 'im_ask_user',
11
+ description: 'Ask the user a question and block until they reply. Equivalent to '
12
+ + '`AskUserQuestion`, routed through the IM channel or dashboard. '
13
+ + 'Supply `options` whenever the answer is enumerable so the user can '
14
+ + 'tap a choice. Use only when you genuinely need user input to '
15
+ + 'proceed; for routine clarifications, pick a sensible default and '
16
+ + 'continue.',
17
+ inputSchema: {
18
+ type: 'object',
19
+ properties: {
20
+ question: { type: 'string', description: 'Question text shown to the user.' },
21
+ header: { type: 'string', description: 'Optional short header (≤ 24 chars).' },
22
+ hint: { type: 'string', description: 'Optional helper text shown alongside the question.' },
23
+ options: {
24
+ type: 'array',
25
+ description: 'Predefined choices. Omit for a freeform answer.',
26
+ items: {
27
+ type: 'object',
28
+ properties: {
29
+ label: { type: 'string', description: 'Option label, returned verbatim if selected.' },
30
+ description: { type: 'string', description: 'Optional secondary description.' },
31
+ },
32
+ required: ['label'],
33
+ },
34
+ },
35
+ allow_freeform: {
36
+ type: 'boolean',
37
+ description: 'When options are supplied, also accept freeform text (default true).',
38
+ },
39
+ },
40
+ required: ['question'],
41
+ },
42
+ },
43
+ ];
44
+ async function handleAskUser(args, ctx) {
45
+ const question = typeof args?.question === 'string' ? args.question.trim() : '';
46
+ const header = typeof args?.header === 'string' ? args.header.trim() : '';
47
+ const hint = typeof args?.hint === 'string' ? args.hint.trim() : '';
48
+ const allowFreeform = args?.allow_freeform == null ? true : !!args.allow_freeform;
49
+ const rawOptions = Array.isArray(args?.options) ? args.options : [];
50
+ const options = rawOptions
51
+ .map((o) => ({
52
+ label: typeof o?.label === 'string' ? o.label.trim() : '',
53
+ description: typeof o?.description === 'string' ? o.description.trim() : '',
54
+ }))
55
+ .filter(o => o.label);
56
+ if (!question) {
57
+ toolLog('im_ask_user', 'ERROR missing question');
58
+ return toolResult('Error: "question" is required', true);
59
+ }
60
+ if (!ctx.callbackUrl) {
61
+ toolLog('im_ask_user', 'ERROR no callback URL');
62
+ return toolResult('Error: MCP callback URL is not configured', true);
63
+ }
64
+ toolLog('im_ask_user', `question="${question.slice(0, 160)}" options=${options.length} freeform=${allowFreeform}`);
65
+ try {
66
+ const response = await callbackAskUser(ctx.callbackUrl, { question, header, hint, allowFreeform, options });
67
+ if (response.ok) {
68
+ const answer = (response.answer || '').trim();
69
+ toolLog('im_ask_user', `OK answer="${answer.slice(0, 160)}"`);
70
+ return toolResult(answer || '(no response)');
71
+ }
72
+ toolLog('im_ask_user', `FAILED ${response.error || 'unknown error'}`);
73
+ return toolResult(`Failed to get user response: ${response.error || 'unknown error'}`, true);
74
+ }
75
+ catch (e) {
76
+ toolLog('im_ask_user', `ERROR ${e.message}`);
77
+ return toolResult(`Error asking user: ${e.message}`, true);
78
+ }
79
+ }
80
+ function callbackAskUser(callbackUrl, body) {
81
+ const payload = JSON.stringify(body);
82
+ const url = new URL('/ask-user', callbackUrl);
83
+ return new Promise((resolve, reject) => {
84
+ const req = http.request(url, {
85
+ method: 'POST',
86
+ headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) },
87
+ }, res => {
88
+ let data = '';
89
+ res.on('data', (chunk) => { data += chunk; });
90
+ res.on('end', () => {
91
+ try {
92
+ resolve(JSON.parse(data));
93
+ }
94
+ catch {
95
+ resolve({ ok: false, error: 'invalid callback response' });
96
+ }
97
+ });
98
+ });
99
+ req.on('error', e => reject(e));
100
+ // User reply has no upper bound — never time out the socket.
101
+ req.setTimeout(0);
102
+ req.write(payload);
103
+ req.end();
104
+ });
105
+ }
106
+ export const askUserTools = {
107
+ tools,
108
+ handle(name, args, ctx) {
109
+ if (name === 'im_ask_user')
110
+ return handleAskUser(args, ctx);
111
+ return toolResult(`Unknown ask-user tool: ${name}`, true);
112
+ },
113
+ };
@@ -0,0 +1,77 @@
1
+ /**
2
+ * tools/await-resume.ts — "waiting for background work" marker MCP tool.
3
+ *
4
+ * await_background — Lets the model declare that this turn is ending while
5
+ * detached/background work it launched keeps running, and
6
+ * it intends to report back later. Purely a UI hint: it
7
+ * changes nothing about execution, it only lets the
8
+ * dashboard show a "waiting" state instead of "completed"
9
+ * for the interval until the session next runs.
10
+ *
11
+ * State lives at <sessionRoot>/awaiting.json; this server resolves the session
12
+ * root from MCP_WORKSPACE_PATH (which points to <sessionRoot>/workspace). The
13
+ * parent reads & clears it (see agent/await-resume.ts) — clearing happens
14
+ * automatically the next time the session runs.
15
+ */
16
+ import fs from 'node:fs';
17
+ import path from 'node:path';
18
+ import { toolResult, toolLog } from './types.js';
19
+ const AWAIT_FILE = 'awaiting.json';
20
+ const MAX_REASON_CHARS = 280;
21
+ const tools = [
22
+ {
23
+ name: 'await_background',
24
+ description: [
25
+ 'Mark this session as waiting on detached background work.',
26
+ 'Call this ONLY when you are ending your turn while work you launched keeps running detached from this turn (a daemon, a build/install that must survive a restart, a long external job) and you intend to report back on it in a later turn.',
27
+ 'This is a passive status hint for the dashboard — it does NOT change how you run, does NOT keep the turn open, and does NOT wake you when the work finishes. Do not call it for ordinary tool calls or foreground work, and do not call it just because a task is large.',
28
+ 'The marker is cleared automatically the next time this session runs, so you never need to clear it yourself.',
29
+ ].join('\n'),
30
+ inputSchema: {
31
+ type: 'object',
32
+ properties: {
33
+ reason: {
34
+ type: 'string',
35
+ description: 'Short, human-readable note on what is still running and what you will report back (e.g. "rebuilding + restarting pikiloop, will confirm after it comes back up").',
36
+ },
37
+ },
38
+ required: ['reason'],
39
+ },
40
+ },
41
+ ];
42
+ function sessionRootFromCtx(ctx) {
43
+ const workspace = path.resolve(ctx.workspace || '');
44
+ if (!workspace)
45
+ return '';
46
+ return path.basename(workspace) === 'workspace' ? path.dirname(workspace) : workspace;
47
+ }
48
+ function handleAwaitBackground(args, ctx) {
49
+ const reason = typeof args?.reason === 'string' ? args.reason.trim().slice(0, MAX_REASON_CHARS) : '';
50
+ toolLog('await_background', `reason=${reason.slice(0, 80) || '(empty)'}`);
51
+ if (!reason) {
52
+ return toolResult('Error: `reason` must be a non-empty description of the background work being awaited.', true);
53
+ }
54
+ const root = sessionRootFromCtx(ctx);
55
+ if (!root)
56
+ return toolResult('Error: MCP workspace path is not configured', true);
57
+ const file = path.join(root, AWAIT_FILE);
58
+ try {
59
+ fs.mkdirSync(root, { recursive: true });
60
+ const tmp = `${file}.tmp-${process.pid}-${Date.now().toString(36)}`;
61
+ fs.writeFileSync(tmp, JSON.stringify({ reason, since: new Date().toISOString() }, null, 2));
62
+ fs.renameSync(tmp, file);
63
+ }
64
+ catch (err) {
65
+ return toolResult(`Error: failed to write awaiting marker: ${err?.message || err}`, true);
66
+ }
67
+ return toolResult('Marked this session as waiting on background work. The dashboard will show a "waiting" state until the session next runs (which clears the marker). Reminder: this does not wake you — you must resume the turn yourself or be re-prompted.');
68
+ }
69
+ export const awaitResumeTools = {
70
+ tools,
71
+ handle(name, args, ctx) {
72
+ switch (name) {
73
+ case 'await_background': return handleAwaitBackground(args, ctx);
74
+ default: return toolResult(`Unknown await tool: ${name}`, true);
75
+ }
76
+ },
77
+ };
@@ -0,0 +1,144 @@
1
+ /**
2
+ * tools/goal.ts — Persistent thread goal MCP tools.
3
+ *
4
+ * goal_get — Returns the current goal (objective, status, usage, budget).
5
+ * goal_update — Lets the model mark the goal complete after a completion audit.
6
+ * The schema only accepts `status: "complete"`. Pause / resume /
7
+ * clear / budget_limited transitions are user- or runtime-controlled
8
+ * and not exposed to the model.
9
+ *
10
+ * Goal state lives at <sessionRoot>/goal.json; this server resolves the session
11
+ * root from MCP_WORKSPACE_PATH (which points to <sessionRoot>/workspace).
12
+ */
13
+ import fs from 'node:fs';
14
+ import path from 'node:path';
15
+ import { toolResult, toolLog } from './types.js';
16
+ // ---------------------------------------------------------------------------
17
+ // Tool definitions — descriptions match Codex's analogous `get_goal` / `update_goal`
18
+ // behaviour but use the pikiloop tool name.
19
+ // ---------------------------------------------------------------------------
20
+ const tools = [
21
+ {
22
+ name: 'goal_get',
23
+ description: 'Get the current goal for this session, including objective, status, token and elapsed-time usage, token budget, and tokens remaining.',
24
+ inputSchema: { type: 'object', properties: {} },
25
+ },
26
+ {
27
+ name: 'goal_update',
28
+ description: [
29
+ 'Update the existing goal.',
30
+ 'Use this tool only to mark the goal achieved.',
31
+ 'Set status to `complete` only when the objective has actually been achieved and no required work remains.',
32
+ 'Do not mark a goal complete merely because its budget is nearly exhausted or because you are stopping work.',
33
+ 'You cannot use this tool to pause, resume, or budget-limit a goal; those status changes are controlled by the user or system.',
34
+ 'When marking a budgeted goal achieved with status `complete`, report the final token usage from the tool result to the user.',
35
+ ].join('\n'),
36
+ inputSchema: {
37
+ type: 'object',
38
+ properties: {
39
+ status: {
40
+ type: 'string',
41
+ enum: ['complete'],
42
+ description: 'Required. Set to "complete" only when the objective is achieved and no required work remains.',
43
+ },
44
+ },
45
+ required: ['status'],
46
+ },
47
+ },
48
+ ];
49
+ // ---------------------------------------------------------------------------
50
+ // Path resolution — workspace → session root → goal.json
51
+ // ---------------------------------------------------------------------------
52
+ function sessionRootFromCtx(ctx) {
53
+ const workspace = path.resolve(ctx.workspace || '');
54
+ if (!workspace)
55
+ return '';
56
+ return path.basename(workspace) === 'workspace' ? path.dirname(workspace) : workspace;
57
+ }
58
+ function goalPathFromCtx(ctx) {
59
+ const root = sessionRootFromCtx(ctx);
60
+ if (!root)
61
+ return '';
62
+ return path.join(root, 'goal.json');
63
+ }
64
+ function readGoalFile(file) {
65
+ if (!file || !fs.existsSync(file))
66
+ return null;
67
+ try {
68
+ return JSON.parse(fs.readFileSync(file, 'utf-8'));
69
+ }
70
+ catch {
71
+ return null;
72
+ }
73
+ }
74
+ function writeGoalFile(file, goal) {
75
+ const dir = path.dirname(file);
76
+ fs.mkdirSync(dir, { recursive: true });
77
+ const tmp = `${file}.tmp-${process.pid}-${Date.now().toString(36)}`;
78
+ fs.writeFileSync(tmp, JSON.stringify({ ...goal, updatedAt: new Date().toISOString() }, null, 2));
79
+ fs.renameSync(tmp, file);
80
+ }
81
+ function serialize(goal) {
82
+ return {
83
+ goal_id: goal.goalId,
84
+ objective: goal.objective,
85
+ status: goal.status,
86
+ token_budget: goal.tokenBudget,
87
+ tokens_used: goal.tokensUsed,
88
+ time_used_seconds: goal.timeUsedSeconds,
89
+ remaining_tokens: goal.tokenBudget != null ? Math.max(0, goal.tokenBudget - goal.tokensUsed) : null,
90
+ created_at: goal.createdAt,
91
+ updated_at: goal.updatedAt,
92
+ };
93
+ }
94
+ // ---------------------------------------------------------------------------
95
+ // Handlers
96
+ // ---------------------------------------------------------------------------
97
+ function handleGoalGet(ctx) {
98
+ const file = goalPathFromCtx(ctx);
99
+ toolLog('goal_get', `file=${file || '(unresolved)'}`);
100
+ if (!file)
101
+ return toolResult('Error: MCP workspace path is not configured', true);
102
+ const goal = readGoalFile(file);
103
+ if (!goal)
104
+ return toolResult(JSON.stringify({ goal: null }, null, 2));
105
+ return toolResult(JSON.stringify({ goal: serialize(goal) }, null, 2));
106
+ }
107
+ function handleGoalUpdate(args, ctx) {
108
+ const status = typeof args?.status === 'string' ? args.status : '';
109
+ toolLog('goal_update', `status=${status}`);
110
+ if (status !== 'complete') {
111
+ return toolResult('Error: goal_update only accepts status "complete". Pause, resume, and budget-limited status changes are controlled by the user or system.', true);
112
+ }
113
+ const file = goalPathFromCtx(ctx);
114
+ if (!file)
115
+ return toolResult('Error: MCP workspace path is not configured', true);
116
+ const goal = readGoalFile(file);
117
+ if (!goal) {
118
+ return toolResult('Error: no goal is currently set for this session', true);
119
+ }
120
+ if (goal.status === 'complete') {
121
+ return toolResult(JSON.stringify({ goal: serialize(goal), note: 'already complete' }, null, 2));
122
+ }
123
+ const next = { ...goal, status: 'complete' };
124
+ writeGoalFile(file, next);
125
+ return toolResult(JSON.stringify({
126
+ goal: serialize(next),
127
+ completion_budget_report: goal.tokenBudget != null
128
+ ? `tokens used: ${goal.tokensUsed} of ${goal.tokenBudget}`
129
+ : `tokens used: ${goal.tokensUsed}`,
130
+ }, null, 2));
131
+ }
132
+ // ---------------------------------------------------------------------------
133
+ // Module export
134
+ // ---------------------------------------------------------------------------
135
+ export const goalTools = {
136
+ tools,
137
+ handle(name, args, ctx) {
138
+ switch (name) {
139
+ case 'goal_get': return handleGoalGet(ctx);
140
+ case 'goal_update': return handleGoalUpdate(args, ctx);
141
+ default: return toolResult(`Unknown goal tool: ${name}`, true);
142
+ }
143
+ },
144
+ };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * MCP tool type definitions and helper utilities.
3
+ */
4
+ import { writeScopedLog } from '../../../core/logging.js';
5
+ /** Helper to build a text tool result. */
6
+ export function toolResult(text, isError = false) {
7
+ return { content: [{ type: 'text', text }], ...(isError ? { isError: true } : {}) };
8
+ }
9
+ /** Shared logger for tool modules — writes to stderr to avoid interfering with stdio MCP transport. */
10
+ export function toolLog(tool, msg) {
11
+ writeScopedLog(`tool:${tool}`, msg, { level: 'debug', stream: 'stderr' });
12
+ }