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.
- package/LICENSE +21 -0
- package/README.md +353 -0
- package/README.v2.md +287 -0
- package/README.zh-CN.md +352 -0
- package/dashboard/dist/assets/AgentTab-UZPIhlkr.js +1 -0
- package/dashboard/dist/assets/DirBrowser-Ckcmi-Pi.js +1 -0
- package/dashboard/dist/assets/ExtensionsTab-KZhEDrdu.js +1 -0
- package/dashboard/dist/assets/IMAccessTab-Bd_IY1GQ.js +1 -0
- package/dashboard/dist/assets/Modal-CTeL0y7P.js +1 -0
- package/dashboard/dist/assets/Modals-axftHasy.js +1 -0
- package/dashboard/dist/assets/Select-C8tOdPhe.js +1 -0
- package/dashboard/dist/assets/SessionPanel-C1geSRxw.js +1 -0
- package/dashboard/dist/assets/SystemTab-DBDkaPiO.js +1 -0
- package/dashboard/dist/assets/anthropic-BAdojD7P.ico +0 -0
- package/dashboard/dist/assets/codex-DYadqqp0.png +0 -0
- package/dashboard/dist/assets/deepseek-BeYNZEk0.ico +0 -0
- package/dashboard/dist/assets/doubao-DloFDuFR.png +0 -0
- package/dashboard/dist/assets/feishu-C4OMrjCW.ico +0 -0
- package/dashboard/dist/assets/gemini-BYkEpiWr.svg +1 -0
- package/dashboard/dist/assets/hermes-BAarh-tH.png +0 -0
- package/dashboard/dist/assets/index-CpM4CqZJ.js +23 -0
- package/dashboard/dist/assets/index-DXSohzrE.js +3 -0
- package/dashboard/dist/assets/index-reSbuley.css +1 -0
- package/dashboard/dist/assets/markdown-DxQYQFeH.js +29 -0
- package/dashboard/dist/assets/minimax-PuEGTfrF.ico +0 -0
- package/dashboard/dist/assets/mlx-DhWwjtMw.png +0 -0
- package/dashboard/dist/assets/ollama-Bt9O-2K_.png +0 -0
- package/dashboard/dist/assets/openrouter-CsJ_bD5Q.ico +0 -0
- package/dashboard/dist/assets/playwright-BldPFZgC.ico +0 -0
- package/dashboard/dist/assets/qwen-xykkX0_y.png +0 -0
- package/dashboard/dist/assets/react-vendor-C7Sl8SE7.js +9 -0
- package/dashboard/dist/assets/router-DHISdpPk.js +3 -0
- package/dashboard/dist/assets/shared-BIP_4k4I.js +1 -0
- package/dashboard/dist/favicon.svg +28 -0
- package/dashboard/dist/index.html +17 -0
- package/dist/agent/acp-client.js +261 -0
- package/dist/agent/auto-update.js +432 -0
- package/dist/agent/await-resume.js +50 -0
- package/dist/agent/cli/auth.js +325 -0
- package/dist/agent/cli/catalog.js +40 -0
- package/dist/agent/cli/detector.js +136 -0
- package/dist/agent/cli/index.js +7 -0
- package/dist/agent/cli/registry.js +33 -0
- package/dist/agent/driver.js +39 -0
- package/dist/agent/drivers/claude-tui.js +2297 -0
- package/dist/agent/drivers/claude.js +2689 -0
- package/dist/agent/drivers/codex.js +2210 -0
- package/dist/agent/drivers/gemini.js +1059 -0
- package/dist/agent/drivers/hermes.js +795 -0
- package/dist/agent/goal.js +274 -0
- package/dist/agent/handover.js +130 -0
- package/dist/agent/images.js +355 -0
- package/dist/agent/index.js +50 -0
- package/dist/agent/mcp/bridge.js +791 -0
- package/dist/agent/mcp/extensions.js +637 -0
- package/dist/agent/mcp/oauth.js +353 -0
- package/dist/agent/mcp/registry.js +119 -0
- package/dist/agent/mcp/session-server.js +229 -0
- package/dist/agent/mcp/tools/ask-user.js +113 -0
- package/dist/agent/mcp/tools/await-resume.js +77 -0
- package/dist/agent/mcp/tools/goal.js +144 -0
- package/dist/agent/mcp/tools/types.js +12 -0
- package/dist/agent/mcp/tools/workspace.js +212 -0
- package/dist/agent/npm.js +31 -0
- package/dist/agent/session.js +1206 -0
- package/dist/agent/skill-installer.js +160 -0
- package/dist/agent/skills.js +257 -0
- package/dist/agent/stream.js +743 -0
- package/dist/agent/types.js +13 -0
- package/dist/agent/utils.js +687 -0
- package/dist/bot/bot.js +2499 -0
- package/dist/bot/command-ui.js +633 -0
- package/dist/bot/commands.js +513 -0
- package/dist/bot/headless-bot.js +36 -0
- package/dist/bot/host.js +192 -0
- package/dist/bot/human-loop.js +168 -0
- package/dist/bot/menu.js +48 -0
- package/dist/bot/orchestration.js +79 -0
- package/dist/bot/render-shared.js +309 -0
- package/dist/bot/session-hub.js +361 -0
- package/dist/bot/session-status.js +55 -0
- package/dist/bot/streaming.js +309 -0
- package/dist/browser-profile.js +579 -0
- package/dist/browser-supervisor.js +249 -0
- package/dist/catalog/cli-tools.js +421 -0
- package/dist/catalog/index.js +21 -0
- package/dist/catalog/local-models.js +94 -0
- package/dist/catalog/mcp-servers.js +315 -0
- package/dist/catalog/skill-repos.js +173 -0
- package/dist/channels/base.js +55 -0
- package/dist/channels/dingtalk/bot.js +549 -0
- package/dist/channels/dingtalk/channel.js +268 -0
- package/dist/channels/discord/bot.js +552 -0
- package/dist/channels/discord/channel.js +245 -0
- package/dist/channels/feishu/bot.js +1275 -0
- package/dist/channels/feishu/channel.js +911 -0
- package/dist/channels/feishu/markdown.js +91 -0
- package/dist/channels/feishu/render.js +619 -0
- package/dist/channels/health.js +109 -0
- package/dist/channels/slack/bot.js +554 -0
- package/dist/channels/slack/channel.js +283 -0
- package/dist/channels/states.js +6 -0
- package/dist/channels/telegram/bot.js +1310 -0
- package/dist/channels/telegram/channel.js +820 -0
- package/dist/channels/telegram/directory.js +111 -0
- package/dist/channels/telegram/live-preview.js +220 -0
- package/dist/channels/telegram/render.js +384 -0
- package/dist/channels/wecom/bot.js +558 -0
- package/dist/channels/wecom/channel.js +479 -0
- package/dist/channels/weixin/api.js +520 -0
- package/dist/channels/weixin/bot.js +1000 -0
- package/dist/channels/weixin/channel.js +222 -0
- package/dist/cli/autostart.js +262 -0
- package/dist/cli/channel-supervisor.js +313 -0
- package/dist/cli/channels.js +54 -0
- package/dist/cli/main.js +726 -0
- package/dist/cli/onboarding.js +227 -0
- package/dist/cli/run.js +308 -0
- package/dist/cli/setup-wizard.js +235 -0
- package/dist/core/config/runtime-config.js +201 -0
- package/dist/core/config/user-config.js +510 -0
- package/dist/core/config/validation.js +521 -0
- package/dist/core/constants.js +400 -0
- package/dist/core/git.js +145 -0
- package/dist/core/legacy-compat.js +60 -0
- package/dist/core/logging.js +101 -0
- package/dist/core/platform.js +59 -0
- package/dist/core/process-control.js +315 -0
- package/dist/core/secrets/index.js +42 -0
- package/dist/core/secrets/inline-seal.js +60 -0
- package/dist/core/secrets/ref.js +33 -0
- package/dist/core/secrets/resolver.js +65 -0
- package/dist/core/secrets/store.js +63 -0
- package/dist/core/utils.js +233 -0
- package/dist/core/version.js +15 -0
- package/dist/dashboard/platform.js +219 -0
- package/dist/dashboard/routes/agents.js +450 -0
- package/dist/dashboard/routes/cli.js +174 -0
- package/dist/dashboard/routes/config.js +523 -0
- package/dist/dashboard/routes/extensions.js +745 -0
- package/dist/dashboard/routes/local-models.js +290 -0
- package/dist/dashboard/routes/models.js +324 -0
- package/dist/dashboard/routes/sessions.js +838 -0
- package/dist/dashboard/runtime.js +410 -0
- package/dist/dashboard/server.js +237 -0
- package/dist/dashboard/session-control.js +347 -0
- package/dist/model/catalog.js +104 -0
- package/dist/model/index.js +20 -0
- package/dist/model/injector.js +272 -0
- package/dist/model/provider-models.js +112 -0
- package/dist/model/store.js +212 -0
- package/dist/model/types.js +13 -0
- package/dist/model/validation.js +203 -0
- 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';
|