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,347 @@
1
+ /**
2
+ * Public session task control surface for dashboard and API routes.
3
+ */
4
+ import path from 'node:path';
5
+ import { getProjectSkillPaths, listSkills, stageSessionFiles, ensureManagedSession, findPikiloopSession, getDriverCapabilities, isPendingSessionId } from '../agent/index.js';
6
+ import { loadUserConfig } from '../core/config/user-config.js';
7
+ import { decomposeEffortSelection } from '../core/config/runtime-config.js';
8
+ import { runtime } from './runtime.js';
9
+ const KNOWN_AGENTS = new Set(['claude', 'codex', 'gemini', 'hermes']);
10
+ /**
11
+ * Parse a `/goal[ args]` prompt typed in the dashboard chat box. Returns null
12
+ * when the prompt is not a goal slash command. Sub-commands mirror the IM
13
+ * `handleGoalCommand` semantics (set / clear / pause / resume / status).
14
+ *
15
+ * Routing /goal through the native bridge is the dashboard's analog of what
16
+ * channels/{telegram,feishu,weixin}/bot.ts do via `handleGoalCommand` — before
17
+ * this hook, dashboard /goal was matched by the legacy `goal` skill resolver
18
+ * and silently rewritten to "Read SKILL.md and execute", which bypassed both
19
+ * the claude native /goal slash command and codex's thread/goal RPC.
20
+ */
21
+ function parseGoalSlash(prompt) {
22
+ const trimmed = prompt.trim();
23
+ const m = trimmed.match(/^\/goal(?:\s+([\s\S]*))?$/);
24
+ if (!m)
25
+ return null;
26
+ const args = (m[1] || '').trim();
27
+ if (!args)
28
+ return { action: 'status', objective: '' };
29
+ const lower = args.toLowerCase();
30
+ if (lower === 'clear' || lower === 'cancel' || lower === 'stop')
31
+ return { action: 'clear', objective: '' };
32
+ if (lower === 'pause')
33
+ return { action: 'pause', objective: '' };
34
+ if (lower === 'resume')
35
+ return { action: 'resume', objective: '' };
36
+ return { action: 'set', objective: args };
37
+ }
38
+ /**
39
+ * Resolve a `/skill-name [args]` prompt into the full skill execution prompt.
40
+ * Returns null if the prompt is not a skill invocation or the skill is not found.
41
+ */
42
+ function resolveSkillFromPrompt(workdir, prompt) {
43
+ const trimmed = prompt.trim();
44
+ if (!trimmed.startsWith('/'))
45
+ return null;
46
+ // Extract command name and args: "/skill-name some args" → name="skill-name", args="some args"
47
+ const match = trimmed.match(/^\/([^\s]+)(?:\s+(.*))?$/s);
48
+ if (!match)
49
+ return null;
50
+ const name = match[1];
51
+ const args = (match[2] || '').trim();
52
+ const { skills } = listSkills(workdir);
53
+ // Match by exact skill name (case-insensitive)
54
+ const skill = skills.find(s => s.name.toLowerCase() === name.toLowerCase());
55
+ if (!skill)
56
+ return null;
57
+ const extra = args ? ` Additional context: ${args}` : '';
58
+ const workdirHint = `[Project directory: ${workdir}]\n\n`;
59
+ const paths = getProjectSkillPaths(workdir, skill.name);
60
+ const skillFile = paths.claudeSkillFile || paths.sharedSkillFile || paths.agentsSkillFile;
61
+ const targetPath = skillFile || `${workdir}/.pikiloop/skills/${skill.name}/SKILL.md`;
62
+ const resolvedPrompt = `${workdirHint}Read the skill definition at \`${targetPath}\` and execute the instructions defined there.${extra}`;
63
+ return { resolvedPrompt, skillName: skill.name };
64
+ }
65
+ /**
66
+ * Resolve a `handoverFrom` ref from the request's `previousAgent` /
67
+ * `previousSessionId` fields, validating that it points to a real, non-self,
68
+ * different-agent session managed by pikiloop. Returns null when the inputs
69
+ * are absent or invalid — handover is best-effort and silent-skip on bad data.
70
+ */
71
+ function resolveHandoverFrom(request, targetAgent) {
72
+ const prevAgent = typeof request.previousAgent === 'string' ? request.previousAgent.trim() : '';
73
+ const prevSessionId = typeof request.previousSessionId === 'string' ? request.previousSessionId.trim() : '';
74
+ if (!prevAgent || !prevSessionId)
75
+ return null;
76
+ if (!KNOWN_AGENTS.has(prevAgent))
77
+ return null;
78
+ if (prevAgent === targetAgent)
79
+ return null; // same-agent continuation goes via --resume, not handover
80
+ if (isPendingSessionId(prevSessionId))
81
+ return null; // no native history yet → nothing to compact
82
+ const record = findPikiloopSession(request.workdir, prevAgent, prevSessionId);
83
+ if (!record)
84
+ return null;
85
+ return { agent: prevAgent, sessionId: prevSessionId };
86
+ }
87
+ export async function queueDashboardSessionTask(request) {
88
+ const bot = runtime.getBotRef();
89
+ if (!bot)
90
+ return { ok: false, error: 'Bot is not running' };
91
+ if (!request.workdir || (!request.prompt && !(request.attachments || []).length)) {
92
+ return { ok: false, error: 'workdir and either prompt or attachments are required' };
93
+ }
94
+ const config = loadUserConfig();
95
+ const resolvedAgent = typeof request.agent === 'string' && KNOWN_AGENTS.has(request.agent)
96
+ ? request.agent
97
+ : runtime.getRuntimeDefaultAgent(config);
98
+ const modelId = typeof request.model === 'string' ? request.model.trim() : '';
99
+ // "ultra" is a synthetic effort rung = max depth + Workflow orchestration;
100
+ // decompose it so the spawn carries a real --effort value plus the workflow
101
+ // flag (the per-send pick is the single knob — no separate workflow control).
102
+ const { effort: splitEffort, workflow: ultraWorkflow } = decomposeEffortSelection(typeof request.effort === 'string' ? request.effort : '');
103
+ const thinkingEffort = resolvedAgent === 'gemini' ? '' : splitEffort;
104
+ const workflowEnabled = ultraWorkflow || request.workflow === true;
105
+ // /goal — route directly to the goal bridge (claude native slash, codex RPC,
106
+ // or portable goal.json for gemini/hermes). Must run BEFORE skill resolution
107
+ // so the legacy `goal` skill doesn't grab the prompt and rewrite it into a
108
+ // "Read SKILL.md" instruction.
109
+ const goalCmd = parseGoalSlash(request.prompt || '');
110
+ if (goalCmd && request.sessionId && !isPendingSessionId(request.sessionId)) {
111
+ return runDashboardGoalSlash(bot, resolvedAgent, request, goalCmd, modelId, thinkingEffort);
112
+ }
113
+ // Resolve /skill-name prompts into full skill execution prompts
114
+ let prompt = request.prompt;
115
+ const skillResult = prompt ? resolveSkillFromPrompt(request.workdir, prompt) : null;
116
+ if (skillResult) {
117
+ prompt = skillResult.resolvedPrompt;
118
+ runtime.debug(`[session-send] resolved skill: ${skillResult.skillName}`);
119
+ }
120
+ let sessionId = request.sessionId;
121
+ let attachments = request.attachments || [];
122
+ // Resolve handover source. Only meaningful when we're about to stage a fresh
123
+ // session (sessionId blank or pending). For an existing session we never
124
+ // replay handover — that session's own --resume history is canonical.
125
+ const isFreshSession = !sessionId || isPendingSessionId(sessionId);
126
+ const handoverFrom = isFreshSession ? resolveHandoverFrom(request, resolvedAgent) : null;
127
+ // Stage files into the session workspace so temp uploads survive cleanup.
128
+ // Also creates a new pending session when no sessionId is provided.
129
+ if (!sessionId || attachments.length) {
130
+ const staged = stageSessionFiles({
131
+ agent: resolvedAgent,
132
+ workdir: request.workdir,
133
+ files: attachments,
134
+ sessionId: sessionId || null,
135
+ title: request.prompt || 'New session',
136
+ threadId: null,
137
+ handoverFrom,
138
+ });
139
+ if (!sessionId)
140
+ sessionId = staged.sessionId;
141
+ if (staged.importedFiles.length) {
142
+ attachments = staged.importedFiles.map(f => path.join(staged.workspacePath, f));
143
+ }
144
+ }
145
+ return bot.submitSessionTask({
146
+ workdir: request.workdir,
147
+ agent: resolvedAgent,
148
+ sessionId,
149
+ prompt: prompt || 'Please inspect the attached file(s).',
150
+ attachments,
151
+ ...(modelId ? { modelId } : {}),
152
+ ...(thinkingEffort ? { thinkingEffort } : {}),
153
+ // Always thread the per-send workflow choice (even when false) so the run
154
+ // explicitly reflects the picked rung (Ultra ⇒ on) rather than any ambient
155
+ // default.
156
+ workflowEnabled,
157
+ ...(handoverFrom ? { handoverFrom } : {}),
158
+ });
159
+ }
160
+ async function runDashboardGoalSlash(bot, agent, request, cmd, modelId, thinkingEffort) {
161
+ const opts = { chatId: 'dashboard', modelId: modelId || undefined, thinkingEffort: thinkingEffort || undefined };
162
+ const sessionKey = `${agent}:${request.sessionId}`;
163
+ // Synthetic task id — for set / clear / resume on agents that internally
164
+ // submit a follow-up task (claude native slash, portable continuation),
165
+ // the real task id is owned by submitSessionTask. The dashboard's SSE
166
+ // stream listener picks that up via session events; this id is just to
167
+ // give the HTTP caller a non-empty taskId field.
168
+ const taskId = `goal-${cmd.action}-${Date.now().toString(36)}`;
169
+ try {
170
+ if (cmd.action === 'status') {
171
+ const goal = await bot.getSessionGoal(request.workdir, agent, request.sessionId);
172
+ return { ok: true, taskId, sessionKey, queued: false, goal };
173
+ }
174
+ if (cmd.action === 'clear') {
175
+ await bot.clearSessionGoal(request.workdir, agent, request.sessionId, opts);
176
+ return { ok: true, taskId, sessionKey, queued: false };
177
+ }
178
+ if (cmd.action === 'pause') {
179
+ const goal = await bot.pauseSessionGoal(request.workdir, agent, request.sessionId);
180
+ return { ok: true, taskId, sessionKey, queued: false, goal };
181
+ }
182
+ if (cmd.action === 'resume') {
183
+ const goal = await bot.resumeSessionGoal(request.workdir, agent, request.sessionId, opts);
184
+ return { ok: true, taskId, sessionKey, queued: false, goal };
185
+ }
186
+ // set
187
+ const goal = await bot.setSessionGoal(request.workdir, agent, request.sessionId, {
188
+ objective: cmd.objective,
189
+ ...opts,
190
+ });
191
+ return { ok: true, taskId, sessionKey, queued: true, goal };
192
+ }
193
+ catch (e) {
194
+ return { ok: false, error: e?.message || String(e) };
195
+ }
196
+ }
197
+ export function forkDashboardSessionTask(request) {
198
+ const bot = runtime.getBotRef();
199
+ if (!bot)
200
+ return { ok: false, error: 'Bot is not running' };
201
+ if (!request.workdir || !request.parentSessionId || !request.prompt) {
202
+ return { ok: false, error: 'workdir, parentSessionId, and prompt are required' };
203
+ }
204
+ if (!KNOWN_AGENTS.has(request.agent)) {
205
+ return { ok: false, error: `Unknown agent: ${request.agent}` };
206
+ }
207
+ const agent = request.agent;
208
+ if (!getDriverCapabilities(agent).fork) {
209
+ return { ok: false, error: `Agent ${agent} does not support fork` };
210
+ }
211
+ const modelId = typeof request.model === 'string' ? request.model.trim() : '';
212
+ // Same "ultra" decomposition as the send path — a forked turn launched at
213
+ // Ultra inherits max depth + Workflow orchestration.
214
+ const { effort: splitEffort, workflow: ultraWorkflow } = decomposeEffortSelection(typeof request.effort === 'string' ? request.effort : '');
215
+ const thinkingEffort = agent === 'gemini' ? '' : splitEffort;
216
+ // Resolve /skill-name shorthand the same way send/queue does, so a forked
217
+ // turn that starts with `/skill-name` runs the skill against the child.
218
+ let prompt = request.prompt;
219
+ const skillResult = prompt ? resolveSkillFromPrompt(request.workdir, prompt) : null;
220
+ if (skillResult)
221
+ prompt = skillResult.resolvedPrompt;
222
+ // Make sure the parent has a managed record so `recordFork` (called after the
223
+ // child stream completes) can write the lineage on both sides. Native-only
224
+ // sessions (started outside pikiloop) won't have a record yet.
225
+ ensureManagedSession({
226
+ agent,
227
+ workdir: request.workdir,
228
+ sessionId: request.parentSessionId,
229
+ });
230
+ // Always create a fresh pending session for the child. stageSessionFiles
231
+ // also handles attachment imports into the new workspace.
232
+ const staged = stageSessionFiles({
233
+ agent,
234
+ workdir: request.workdir,
235
+ files: request.attachments || [],
236
+ sessionId: null,
237
+ title: request.prompt || `Fork from ${request.parentSessionId.slice(0, 8)}`,
238
+ threadId: null,
239
+ });
240
+ const attachments = staged.importedFiles.length
241
+ ? staged.importedFiles.map(f => path.join(staged.workspacePath, f))
242
+ : [];
243
+ return bot.submitSessionTask({
244
+ workdir: request.workdir,
245
+ agent,
246
+ sessionId: staged.sessionId,
247
+ prompt: prompt || 'Please inspect the attached file(s).',
248
+ attachments,
249
+ forkOf: { parentSessionId: request.parentSessionId, atTurn: request.atTurn },
250
+ ...(modelId ? { modelId } : {}),
251
+ ...(thinkingEffort ? { thinkingEffort } : {}),
252
+ ...(ultraWorkflow ? { workflowEnabled: true } : {}),
253
+ });
254
+ }
255
+ export function getSessionStreamState(agent, sessionId) {
256
+ const bot = runtime.getBotRef();
257
+ if (!bot)
258
+ return { ok: true, state: null };
259
+ return { ok: true, state: bot.getStreamSnapshot(`${agent}:${sessionId}`) };
260
+ }
261
+ export function cancelSessionTask(taskId) {
262
+ const bot = runtime.getBotRef();
263
+ if (!bot)
264
+ return { ok: false, error: 'Bot is not running' };
265
+ const result = bot.cancelTask(taskId);
266
+ return { ok: true, recalled: result.cancelled || result.interrupted };
267
+ }
268
+ /**
269
+ * Stop only the currently running task for a session — queued follow-ups are
270
+ * preserved and run normally once the chain advances. Works on (agent,
271
+ * sessionId) rather than a single taskId so it still functions during the
272
+ * brief window after send/before the queued WS snapshot reaches the client.
273
+ * Per-row × buttons (→ cancelSessionTask) cancel one queued entry at a time.
274
+ */
275
+ export function stopSessionTasks(agent, sessionId) {
276
+ const bot = runtime.getBotRef();
277
+ if (!bot)
278
+ return { ok: false, error: 'Bot is not running' };
279
+ const result = bot.stopAllSessionTasks(`${agent}:${sessionId}`);
280
+ return { ok: true, ...result };
281
+ }
282
+ export async function steerSessionTask(taskId) {
283
+ const bot = runtime.getBotRef();
284
+ if (!bot)
285
+ return { ok: false, error: 'Bot is not running' };
286
+ const result = await bot.steerTask(taskId);
287
+ return { ok: true, steered: result.steered };
288
+ }
289
+ // ---------------------------------------------------------------------------
290
+ // Interaction prompt control (human-in-the-loop)
291
+ // ---------------------------------------------------------------------------
292
+ export function interactionSelectOption(promptId, optionValue, opts) {
293
+ const bot = runtime.getBotRef();
294
+ if (!bot)
295
+ return { ok: false, error: 'Bot is not running' };
296
+ const result = bot.interactionSelectOption(promptId, optionValue, opts);
297
+ if (!result)
298
+ return { ok: false, error: 'Prompt not found or no longer active' };
299
+ return { ok: true, completed: result.completed, advanced: result.advanced };
300
+ }
301
+ export function interactionSubmitText(promptId, text) {
302
+ const bot = runtime.getBotRef();
303
+ if (!bot)
304
+ return { ok: false, error: 'Bot is not running' };
305
+ const result = bot.interactionSubmitText(promptId, text);
306
+ if (!result)
307
+ return { ok: false, error: 'Prompt not found or not awaiting text' };
308
+ return { ok: true, completed: result.completed, advanced: result.advanced };
309
+ }
310
+ export function interactionSkip(promptId) {
311
+ const bot = runtime.getBotRef();
312
+ if (!bot)
313
+ return { ok: false, error: 'Bot is not running' };
314
+ const result = bot.interactionSkip(promptId);
315
+ if (!result)
316
+ return { ok: false, error: 'Prompt not found or no longer active' };
317
+ return { ok: true, completed: result.completed, advanced: result.advanced };
318
+ }
319
+ export function interactionCancel(promptId) {
320
+ const bot = runtime.getBotRef();
321
+ if (!bot)
322
+ return { ok: false, error: 'Bot is not running' };
323
+ const result = bot.interactionCancel(promptId);
324
+ if (!result)
325
+ return { ok: false, error: 'Prompt not found or no longer active' };
326
+ return { ok: true };
327
+ }
328
+ export function getInteractionPrompt(promptId) {
329
+ const bot = runtime.getBotRef();
330
+ if (!bot)
331
+ return { ok: false, error: 'Bot is not running' };
332
+ const prompt = bot.interactionPrompt(promptId);
333
+ if (!prompt)
334
+ return { ok: true, prompt: null };
335
+ return {
336
+ ok: true,
337
+ prompt: {
338
+ promptId: prompt.promptId,
339
+ taskId: prompt.taskId,
340
+ title: prompt.title,
341
+ hint: prompt.hint,
342
+ questions: prompt.questions,
343
+ currentIndex: prompt.currentIndex,
344
+ answers: prompt.answers,
345
+ },
346
+ };
347
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * models.dev catalog — read-only metadata about LLM providers and their
3
+ * models (pricing, context window, capabilities). We hit the public JSON
4
+ * endpoint and cache the result locally for 24h, with a fallback to the
5
+ * cached copy when offline.
6
+ */
7
+ import fs from 'node:fs';
8
+ import os from 'node:os';
9
+ import path from 'node:path';
10
+ import { request } from 'undici';
11
+ import { STATE_DIR_NAME } from '../core/constants.js';
12
+ const MODELS_DEV_URL = 'https://models.dev/api.json';
13
+ const CACHE_PATH = path.join(os.homedir(), STATE_DIR_NAME, 'models-dev-cache.json');
14
+ const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
15
+ const FETCH_TIMEOUT_MS = 8_000;
16
+ let memCache = null;
17
+ let inflight = null;
18
+ function readDiskCache() {
19
+ try {
20
+ const raw = fs.readFileSync(CACHE_PATH, 'utf8');
21
+ const parsed = JSON.parse(raw);
22
+ if (parsed && typeof parsed === 'object' && parsed.fetchedAt && parsed.data) {
23
+ return parsed;
24
+ }
25
+ }
26
+ catch { }
27
+ return null;
28
+ }
29
+ function writeDiskCache(env) {
30
+ try {
31
+ fs.mkdirSync(path.dirname(CACHE_PATH), { recursive: true });
32
+ fs.writeFileSync(CACHE_PATH, JSON.stringify(env), { mode: 0o644 });
33
+ }
34
+ catch { }
35
+ }
36
+ async function fetchFromNetwork() {
37
+ const { body, statusCode } = await request(MODELS_DEV_URL, {
38
+ method: 'GET',
39
+ headersTimeout: FETCH_TIMEOUT_MS,
40
+ bodyTimeout: FETCH_TIMEOUT_MS,
41
+ });
42
+ if (statusCode < 200 || statusCode >= 300) {
43
+ throw new Error(`models.dev returned HTTP ${statusCode}`);
44
+ }
45
+ const text = await body.text();
46
+ return JSON.parse(text);
47
+ }
48
+ /**
49
+ * Get the catalog. Returns a cached copy if fresh; otherwise fetches in the
50
+ * background and falls back to the stale cache on failure.
51
+ */
52
+ export async function getModelsDevCatalog(opts = {}) {
53
+ const now = Date.now();
54
+ if (!opts.forceRefresh && memCache && now - memCache.fetchedAt < CACHE_TTL_MS) {
55
+ return memCache.data;
56
+ }
57
+ if (!memCache)
58
+ memCache = readDiskCache();
59
+ if (!opts.forceRefresh && memCache && now - memCache.fetchedAt < CACHE_TTL_MS) {
60
+ return memCache.data;
61
+ }
62
+ if (!inflight) {
63
+ inflight = (async () => {
64
+ try {
65
+ const data = await fetchFromNetwork();
66
+ const env = { fetchedAt: Date.now(), data };
67
+ memCache = env;
68
+ writeDiskCache(env);
69
+ return data;
70
+ }
71
+ catch (e) {
72
+ if (memCache)
73
+ return memCache.data; // fall back to stale cache
74
+ throw e;
75
+ }
76
+ finally {
77
+ inflight = null;
78
+ }
79
+ })();
80
+ }
81
+ return inflight;
82
+ }
83
+ /** Lookup a single provider by its models.dev id (e.g. "openrouter"). */
84
+ export async function getCatalogProvider(providerId) {
85
+ const cat = await getModelsDevCatalog().catch(() => null);
86
+ return cat?.[providerId] || null;
87
+ }
88
+ /** Lookup a model entry within a provider. */
89
+ export async function getCatalogModel(providerId, modelId) {
90
+ const provider = await getCatalogProvider(providerId);
91
+ return provider?.models?.[modelId] || null;
92
+ }
93
+ /**
94
+ * Lightweight search: returns providers whose id/name match the query.
95
+ * If query is empty, returns all providers sorted by id.
96
+ */
97
+ export async function searchCatalogProviders(query) {
98
+ const cat = await getModelsDevCatalog().catch(() => ({}));
99
+ const all = Object.values(cat);
100
+ const q = query.trim().toLowerCase();
101
+ if (!q)
102
+ return all.sort((a, b) => a.id.localeCompare(b.id));
103
+ return all.filter(p => p.id.toLowerCase().includes(q) || p.name.toLowerCase().includes(q));
104
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Pikiloop "Model" layer — barrel export.
3
+ *
4
+ * The Model layer is one of the four physical layers in pikiloop's
5
+ * architecture (Terminal / Agent / **Model** / Tool). It centralises:
6
+ * - Provider/Profile data model (types.ts)
7
+ * - Read-only catalog of providers/models from models.dev (catalog.ts)
8
+ * - Persistence (store.ts) over ~/.pikiloop/setting.json
9
+ * - Feishu-style credential validation (validation.ts)
10
+ * - Per-agent credential injection at spawn time (injector.ts)
11
+ *
12
+ * Adding a new agent driver only needs to:
13
+ * 1. Define a new AgentInjector entry in injector.ts
14
+ * 2. Read `resolveAgentInjection(agentId)` before spawning
15
+ */
16
+ export { getModelsDevCatalog, getCatalogProvider, getCatalogModel, searchCatalogProviders, } from './catalog.js';
17
+ export { listProviders, getProvider, addProvider, updateProvider, removeProvider, setProviderValidation, listProfiles, getProfile, addProfile, updateProfile, removeProfile, getActiveProfileId, getActiveProfile, setActiveProfile, } from './store.js';
18
+ export { validateProvider } from './validation.js';
19
+ export { resolveAgentInjection, isAgentBoundToProfile, } from './injector.js';
20
+ export { getProviderModelList, invalidateProviderModels, peekProviderModelList, peekProviderModelInfo, prefetchProviderModels, } from './provider-models.js';