pikiloom 0.4.40 → 0.4.42
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/dashboard/dist/assets/AgentTab-B9z4W2mh.js +1 -0
- package/dashboard/dist/assets/{ConnectionModal-g_6IHAWE.js → ConnectionModal-2bL7sFKk.js} +1 -1
- package/dashboard/dist/assets/{DirBrowser-DVe6q12N.js → DirBrowser-CA4k8E_A.js} +1 -1
- package/dashboard/dist/assets/{ExtensionsTab-GqxD61Pp.js → ExtensionsTab-CRjTmFRl.js} +1 -1
- package/dashboard/dist/assets/{IMAccessTab-DIqTzZJ9.js → IMAccessTab-DDb7rsHe.js} +1 -1
- package/dashboard/dist/assets/{Modal-CIYrMwm0.js → Modal-DDiBG4kb.js} +1 -1
- package/dashboard/dist/assets/{Modals-B-Afu2rD.js → Modals-CsUQjpxn.js} +1 -1
- package/dashboard/dist/assets/{Select-B1UL27gw.js → Select-CG_7h_Tz.js} +1 -1
- package/dashboard/dist/assets/SessionPanel-B5964GUj.js +1 -0
- package/dashboard/dist/assets/{SystemTab-Bv1uT5-h.js → SystemTab-D7r1PsC9.js} +1 -1
- package/dashboard/dist/assets/index-BbplxgnQ.js +3 -0
- package/dashboard/dist/assets/index-CULTTvtY.js +23 -0
- package/dashboard/dist/assets/index-DwmXPtDd.css +1 -0
- package/dashboard/dist/assets/{shared-624PaUfm.js → shared-Bv8WvQSo.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dist/agent/accounts.js +181 -0
- package/dist/agent/drivers/claude.js +108 -3
- package/dist/agent/kernel-bridge.js +45 -1
- package/dist/agent/session.js +6 -2
- package/dist/agent/stream.js +26 -0
- package/dist/bot/bot.js +45 -10
- package/dist/bot/command-ui.js +69 -4
- package/dist/bot/render-shared.js +2 -1
- package/dist/cli/kernel-app.js +4 -2
- package/dist/core/config/runtime-config.js +14 -22
- package/dist/dashboard/routes/accounts.js +113 -0
- package/dist/dashboard/server.js +2 -0
- package/dist/model/responses-bridge.js +37 -0
- package/package.json +1 -1
- package/packages/kernel/README.md +35 -0
- package/packages/kernel/dist/accounts.d.ts +6 -0
- package/packages/kernel/dist/accounts.js +29 -0
- package/packages/kernel/dist/contracts/driver.d.ts +15 -0
- package/packages/kernel/dist/contracts/ports.d.ts +2 -0
- package/packages/kernel/dist/drivers/acp.d.ts +24 -0
- package/packages/kernel/dist/drivers/acp.js +477 -0
- package/packages/kernel/dist/drivers/claude.d.ts +5 -1
- package/packages/kernel/dist/drivers/claude.js +4 -0
- package/packages/kernel/dist/drivers/codex.d.ts +5 -1
- package/packages/kernel/dist/drivers/codex.js +24 -12
- package/packages/kernel/dist/drivers/gemini.d.ts +5 -1
- package/packages/kernel/dist/drivers/gemini.js +4 -0
- package/packages/kernel/dist/drivers/hermes.d.ts +3 -12
- package/packages/kernel/dist/drivers/hermes.js +8 -191
- package/packages/kernel/dist/drivers/index.d.ts +1 -0
- package/packages/kernel/dist/drivers/index.js +1 -0
- package/packages/kernel/dist/index.d.ts +4 -1
- package/packages/kernel/dist/index.js +5 -0
- package/packages/kernel/dist/ports/defaults.js +7 -1
- package/packages/kernel/dist/runtime/loom.d.ts +10 -0
- package/packages/kernel/dist/runtime/loom.js +18 -2
- package/packages/kernel/dist/workspace/index.d.ts +5 -0
- package/packages/kernel/dist/workspace/index.js +7 -0
- package/packages/kernel/dist/workspace/mcp.d.ts +44 -0
- package/packages/kernel/dist/workspace/mcp.js +82 -0
- package/packages/kernel/dist/workspace/native.d.ts +14 -0
- package/packages/kernel/dist/workspace/native.js +340 -0
- package/packages/kernel/dist/workspace/paths.d.ts +33 -0
- package/packages/kernel/dist/workspace/paths.js +40 -0
- package/packages/kernel/dist/workspace/sessions.d.ts +49 -0
- package/packages/kernel/dist/workspace/sessions.js +130 -0
- package/packages/kernel/dist/workspace/skills.d.ts +44 -0
- package/packages/kernel/dist/workspace/skills.js +173 -0
- package/dashboard/dist/assets/AgentTab-Cj7sekcS.js +0 -1
- package/dashboard/dist/assets/SessionPanel-BHuG5hxj.js +0 -1
- package/dashboard/dist/assets/index-BOEW0RIx.js +0 -23
- package/dashboard/dist/assets/index-Bthwt6K_.css +0 -1
- package/dashboard/dist/assets/index-qKexKdJl.js +0 -3
package/dist/bot/command-ui.js
CHANGED
|
@@ -2,6 +2,7 @@ import { normalizeAgent } from './bot.js';
|
|
|
2
2
|
import { getDriverCapabilities } from '../agent/driver.js';
|
|
3
3
|
import { getSessionStatusForChat } from './session-status.js';
|
|
4
4
|
import { getAgentsListData, getModelsListData, getSessionsPageData, getSkillsListData, summarizeSessionRun, modelMatchesSelection, resolveSkillPrompt, } from './commands.js';
|
|
5
|
+
import { accountAgentSupported, listAccounts, getAccount, getActiveAccountId, setActiveAccount, warmAccountUsages, accountUsageSummary, } from '../agent/accounts.js';
|
|
5
6
|
function chunkRows(items, columns) {
|
|
6
7
|
const rows = [];
|
|
7
8
|
const size = Math.max(1, columns);
|
|
@@ -28,6 +29,8 @@ export function encodeCommandAction(action) {
|
|
|
28
29
|
return `sess:${action.sessionId}`;
|
|
29
30
|
case 'agent.switch':
|
|
30
31
|
return `ag:${action.agent}`;
|
|
32
|
+
case 'agent.account.set':
|
|
33
|
+
return `agacc:${action.agent}:${action.accountId ?? ''}`;
|
|
31
34
|
case 'model.switch':
|
|
32
35
|
return `mod:${action.modelId}`;
|
|
33
36
|
case 'effort.set':
|
|
@@ -63,6 +66,18 @@ export function decodeCommandAction(data) {
|
|
|
63
66
|
return null;
|
|
64
67
|
return { kind: 'session.switch', sessionId };
|
|
65
68
|
}
|
|
69
|
+
if (data.startsWith('agacc:')) {
|
|
70
|
+
const rest = data.slice(6);
|
|
71
|
+
const sep = rest.indexOf(':');
|
|
72
|
+
if (sep < 0)
|
|
73
|
+
return null;
|
|
74
|
+
try {
|
|
75
|
+
return { kind: 'agent.account.set', agent: normalizeAgent(rest.slice(0, sep)), accountId: rest.slice(sep + 1) || null };
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
66
81
|
if (data.startsWith('ag:')) {
|
|
67
82
|
try {
|
|
68
83
|
return { kind: 'agent.switch', agent: normalizeAgent(data.slice(3)) };
|
|
@@ -194,15 +209,43 @@ export function buildAgentsCommandView(bot, chatId) {
|
|
|
194
209
|
};
|
|
195
210
|
});
|
|
196
211
|
const current = installed.find(a => a.isCurrent);
|
|
212
|
+
// For the current agent, if it has local accounts (claude), surface them so the user can
|
|
213
|
+
// switch the active account right here — labelled with 👤 and each account's live usage.
|
|
214
|
+
const accountRows = [];
|
|
215
|
+
const accountItems = [];
|
|
216
|
+
const curAgent = data.currentAgent;
|
|
217
|
+
if (accountAgentSupported(curAgent)) {
|
|
218
|
+
const accs = listAccounts(curAgent);
|
|
219
|
+
if (accs.length) {
|
|
220
|
+
warmAccountUsages(curAgent);
|
|
221
|
+
const activeAccId = getActiveAccountId(curAgent);
|
|
222
|
+
const entry = (label, accountId, summary) => {
|
|
223
|
+
const isActive = accountId === activeAccId;
|
|
224
|
+
accountRows.push([{
|
|
225
|
+
label: summary ? `👤 ${label} · ${summary}` : `👤 ${label}`,
|
|
226
|
+
action: { kind: 'agent.account.set', agent: curAgent, accountId },
|
|
227
|
+
state: isActive ? 'current' : 'default',
|
|
228
|
+
primary: isActive,
|
|
229
|
+
}]);
|
|
230
|
+
accountItems.push({ label: `👤 ${label}`, detail: summary, state: isActive ? 'current' : 'default' });
|
|
231
|
+
};
|
|
232
|
+
for (const acc of accs)
|
|
233
|
+
entry(acc.label, acc.id, accountUsageSummary(curAgent, acc.id));
|
|
234
|
+
entry('Default login', null, null);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const helperText = actions.length
|
|
238
|
+
? (accountRows.length ? 'Tap an agent to switch · 👤 = account' : 'Tap an agent to switch.')
|
|
239
|
+
: undefined;
|
|
197
240
|
return {
|
|
198
241
|
kind: 'agents',
|
|
199
242
|
title: 'Agents',
|
|
200
243
|
detail: current ? current.label : undefined,
|
|
201
244
|
metaLines: current ? [`Current: ${current.label}`] : [],
|
|
202
|
-
items,
|
|
245
|
+
items: [...items, ...accountItems],
|
|
203
246
|
emptyText: actions.length ? undefined : 'No installed agents.',
|
|
204
|
-
helperText
|
|
205
|
-
rows: actions.map(action => [action]),
|
|
247
|
+
helperText,
|
|
248
|
+
rows: [...actions.map(action => [action]), ...accountRows],
|
|
206
249
|
};
|
|
207
250
|
}
|
|
208
251
|
const modelsDrafts = new Map();
|
|
@@ -434,9 +477,18 @@ export async function executeCommandAction(bot, chatId, action, opts = {}) {
|
|
|
434
477
|
}
|
|
435
478
|
case 'agent.switch': {
|
|
436
479
|
const chat = bot.chat(chatId);
|
|
437
|
-
|
|
480
|
+
const hasAccounts = accountAgentSupported(action.agent) && listAccounts(action.agent).length > 0;
|
|
481
|
+
if (chat.agent === action.agent) {
|
|
482
|
+
// Already current: reopen the menu (now showing accounts) if it has any, else no-op.
|
|
483
|
+
if (hasAccounts)
|
|
484
|
+
return { kind: 'view', view: buildAgentsCommandView(bot, chatId), callbackText: '' };
|
|
438
485
|
return { kind: 'noop', message: `Already using ${action.agent}` };
|
|
486
|
+
}
|
|
439
487
|
bot.switchAgentForChat(chatId, action.agent);
|
|
488
|
+
// For an account-capable agent, drop the user straight into its account picker.
|
|
489
|
+
if (hasAccounts) {
|
|
490
|
+
return { kind: 'view', view: buildAgentsCommandView(bot, chatId), callbackText: `Switched to ${action.agent}` };
|
|
491
|
+
}
|
|
440
492
|
const resumed = bot.selectedSession(chatId);
|
|
441
493
|
return {
|
|
442
494
|
kind: 'notice',
|
|
@@ -453,6 +505,19 @@ export async function executeCommandAction(bot, chatId, action, opts = {}) {
|
|
|
453
505
|
: null,
|
|
454
506
|
};
|
|
455
507
|
}
|
|
508
|
+
case 'agent.account.set': {
|
|
509
|
+
if (!accountAgentSupported(action.agent))
|
|
510
|
+
return { kind: 'noop', message: `${action.agent} has no accounts` };
|
|
511
|
+
try {
|
|
512
|
+
setActiveAccount(action.agent, action.accountId);
|
|
513
|
+
}
|
|
514
|
+
catch (e) {
|
|
515
|
+
return { kind: 'noop', message: e?.message || 'Switch failed' };
|
|
516
|
+
}
|
|
517
|
+
warmAccountUsages(action.agent);
|
|
518
|
+
const label = action.accountId ? (getAccount(action.agent, action.accountId)?.label ?? action.accountId) : 'Default login';
|
|
519
|
+
return { kind: 'view', view: buildAgentsCommandView(bot, chatId), callbackText: `Account → ${label}` };
|
|
520
|
+
}
|
|
456
521
|
case 'model.switch': {
|
|
457
522
|
const chat = bot.chat(chatId);
|
|
458
523
|
const currentModel = bot.modelForAgent(chat.agent);
|
|
@@ -3,6 +3,7 @@ import { fmtUptime, formatThinkingForDisplay, thinkLabel } from './bot.js';
|
|
|
3
3
|
import { formatActivityCommandSummary, parseActivitySummary, renderPlanForPreview, summarizeActivityForPreview } from './streaming.js';
|
|
4
4
|
import { supportsChannelCapability } from '../channels/base.js';
|
|
5
5
|
import { agentLog, agentWarn } from '../agent/index.js';
|
|
6
|
+
import { effortLabel } from '../core/config/runtime-config.js';
|
|
6
7
|
export function parseGfmTable(tableLines) {
|
|
7
8
|
if (tableLines.length < 3)
|
|
8
9
|
return null;
|
|
@@ -53,7 +54,7 @@ export function formatFooterParts(agent, elapsedMs, meta, contextPercent, decora
|
|
|
53
54
|
identityParts.push(compactModelLabel(modelLabel));
|
|
54
55
|
const runtimeParts = [];
|
|
55
56
|
if (decorations?.effort)
|
|
56
|
-
runtimeParts.push(decorations.effort);
|
|
57
|
+
runtimeParts.push(effortLabel(decorations.effort));
|
|
57
58
|
const ctx = contextPercent ?? meta?.contextPercent ?? null;
|
|
58
59
|
if (ctx != null)
|
|
59
60
|
runtimeParts.push(`${ctx}%`);
|
package/dist/cli/kernel-app.js
CHANGED
|
@@ -19,13 +19,14 @@ const DEMO_MODELS = {
|
|
|
19
19
|
claude: [{ id: 'claude-opus-4-8', label: 'Opus 4.8', providerName: 'anthropic', contextWindow: 200000 }],
|
|
20
20
|
codex: [{ id: 'gpt-5.5-codex', label: 'GPT-5.5 Codex', providerName: 'openai', contextWindow: 400000 }],
|
|
21
21
|
gemini: [{ id: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro', providerName: 'google', contextWindow: 1000000 }],
|
|
22
|
+
opencode: [{ id: 'claude-sonnet-4-6', label: 'OpenCode · Sonnet 4.6 (ACP)', providerName: 'opencode', contextWindow: 200000 }],
|
|
22
23
|
echo: [{ id: 'echo-1', label: 'Echo (hermetic)', providerName: 'local', contextWindow: 8192 }],
|
|
23
24
|
};
|
|
24
25
|
export async function runKernelApp(argv) {
|
|
25
26
|
const i = argv.indexOf('--dashboard-port');
|
|
26
27
|
const port = (i >= 0 && parseInt(argv[i + 1], 10)) || 3940;
|
|
27
28
|
const k = await loadKernel();
|
|
28
|
-
const { createLoom, ClaudeDriver, CodexDriver, GeminiDriver, HermesDriver, EchoDriver, WebSurface } = k;
|
|
29
|
+
const { createLoom, ClaudeDriver, CodexDriver, GeminiDriver, HermesDriver, AcpDriver, EchoDriver, WebSurface } = k;
|
|
29
30
|
// Load providers/profiles so the active BYOK/豆包 bindings are visible to the resolver.
|
|
30
31
|
try {
|
|
31
32
|
applyUserConfig(loadUserConfig(), undefined, { overwrite: true, clearMissing: true });
|
|
@@ -100,7 +101,8 @@ export async function runKernelApp(argv) {
|
|
|
100
101
|
const surfaces = [web];
|
|
101
102
|
const loom = createLoom({
|
|
102
103
|
appNamespace: 'pikiloom-kernel',
|
|
103
|
-
|
|
104
|
+
// OpenCode (and any other ACP CLI) plugs in via the generic AcpDriver — same registry as the natives.
|
|
105
|
+
drivers: [new EchoDriver(), new ClaudeDriver(), new CodexDriver(), new GeminiDriver(), new HermesDriver(), new AcpDriver({ id: 'opencode', command: 'opencode', args: ['acp'] })],
|
|
104
106
|
defaultAgent: 'claude',
|
|
105
107
|
surfaces,
|
|
106
108
|
catalog,
|
|
@@ -164,34 +164,26 @@ export function decomposeEffortSelection(raw) {
|
|
|
164
164
|
return { effort: 'max', workflow: true };
|
|
165
165
|
return { effort: value, workflow: false };
|
|
166
166
|
}
|
|
167
|
+
// The one canonical, human-facing name for an effort token. The picker (effortOptionsFor →
|
|
168
|
+
// dashboard composer + IM /models), the dashboard turn header, and the IM agent-status row all
|
|
169
|
+
// render this, so a level reads identically on every surface — no upper layer relabels it. We
|
|
170
|
+
// surface the raw token (low/medium/high/xhigh/max/ultra/minimal), not a prose label like
|
|
171
|
+
// "Very High", to match the id used in config, CLI flags and logs. Change presentation here only.
|
|
172
|
+
export function effortLabel(id) {
|
|
173
|
+
return (id ?? '').trim().toLowerCase();
|
|
174
|
+
}
|
|
167
175
|
// Single source of truth for reasoning-effort levels, ordered low→high. BOTH the dashboard
|
|
168
176
|
// (which receives them via the agent/model API payload) and the IM bot consume this — do not
|
|
169
177
|
// reintroduce a second copy. Resolve options through effortOptionsFor(), never index directly,
|
|
170
|
-
// so per-model/provider rules stay in one place.
|
|
178
|
+
// so per-model/provider rules stay in one place. Labels come from effortLabel() so the picker
|
|
179
|
+
// and the live status row can never diverge.
|
|
180
|
+
const effortLevels = (...ids) => ids.map(id => ({ id, label: effortLabel(id) }));
|
|
171
181
|
const AGENT_EFFORT_LEVELS = {
|
|
172
|
-
claude:
|
|
173
|
-
|
|
174
|
-
{ id: 'medium', label: 'Medium' },
|
|
175
|
-
{ id: 'high', label: 'High' },
|
|
176
|
-
{ id: 'xhigh', label: 'Very High' },
|
|
177
|
-
{ id: 'max', label: 'Max' },
|
|
178
|
-
{ id: ULTRA_EFFORT, label: 'Ultra' },
|
|
179
|
-
],
|
|
180
|
-
codex: [
|
|
181
|
-
{ id: 'low', label: 'Low' },
|
|
182
|
-
{ id: 'medium', label: 'Medium' },
|
|
183
|
-
{ id: 'high', label: 'High' },
|
|
184
|
-
{ id: 'xhigh', label: 'Very High' },
|
|
185
|
-
],
|
|
182
|
+
claude: effortLevels('low', 'medium', 'high', 'xhigh', 'max', ULTRA_EFFORT),
|
|
183
|
+
codex: effortLevels('low', 'medium', 'high', 'xhigh'),
|
|
186
184
|
// gemini intentionally has no UI-exposed effort levels: pikiloom sends it no reasoning-effort
|
|
187
185
|
// (see the gemini→null guards in InputComposer). Add a gemini entry here to surface low/high.
|
|
188
|
-
hermes:
|
|
189
|
-
{ id: 'minimal', label: 'Minimal' },
|
|
190
|
-
{ id: 'low', label: 'Low' },
|
|
191
|
-
{ id: 'medium', label: 'Medium' },
|
|
192
|
-
{ id: 'high', label: 'High' },
|
|
193
|
-
{ id: 'xhigh', label: 'Very High' },
|
|
194
|
-
],
|
|
186
|
+
hermes: effortLevels('minimal', 'low', 'medium', 'high', 'xhigh'),
|
|
195
187
|
};
|
|
196
188
|
// Valid effort levels for a given (agent, model, providerKind). Returns [] when reasoning
|
|
197
189
|
// effort does not apply (the UI then hides the selector entirely). model/providerKind are the
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { allDriverIds } from '../../agent/index.js';
|
|
3
|
+
import { accountAgentSupported, listAccounts, getAccount, addAccount, updateAccount, removeAccount, getActiveAccountId, setActiveAccount, probeAccountUsage, MAX_ACCOUNTS_PER_AGENT, } from '../../agent/accounts.js';
|
|
4
|
+
const app = new Hono();
|
|
5
|
+
function agentError(agent) {
|
|
6
|
+
if (!allDriverIds().includes(agent))
|
|
7
|
+
return { status: 400, msg: `Unknown agent: ${agent}` };
|
|
8
|
+
if (!accountAgentSupported(agent))
|
|
9
|
+
return { status: 400, msg: `Agent ${agent} does not support multiple accounts` };
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
// Never returns the token — only the display name, usage, and active flag.
|
|
13
|
+
async function publicAccount(agent, rec, activeId) {
|
|
14
|
+
return {
|
|
15
|
+
id: rec.id,
|
|
16
|
+
label: rec.label,
|
|
17
|
+
createdAt: rec.createdAt,
|
|
18
|
+
lastUsedAt: rec.lastUsedAt ?? null,
|
|
19
|
+
active: rec.id === activeId,
|
|
20
|
+
usage: await probeAccountUsage(agent, rec.id),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
app.get('/api/agents/:agent/accounts', async (c) => {
|
|
24
|
+
const agent = c.req.param('agent');
|
|
25
|
+
if (!allDriverIds().includes(agent))
|
|
26
|
+
return c.json({ ok: false, error: `Unknown agent: ${agent}` }, 400);
|
|
27
|
+
if (!accountAgentSupported(agent)) {
|
|
28
|
+
return c.json({ ok: true, agent, supported: false, accounts: [], activeAccountId: null, max: MAX_ACCOUNTS_PER_AGENT });
|
|
29
|
+
}
|
|
30
|
+
const activeId = getActiveAccountId(agent);
|
|
31
|
+
const accounts = await Promise.all(listAccounts(agent).map(r => publicAccount(agent, r, activeId)));
|
|
32
|
+
return c.json({ ok: true, agent, supported: true, accounts, activeAccountId: activeId, max: MAX_ACCOUNTS_PER_AGENT });
|
|
33
|
+
});
|
|
34
|
+
app.post('/api/agents/:agent/accounts', async (c) => {
|
|
35
|
+
const agent = c.req.param('agent');
|
|
36
|
+
const err = agentError(agent);
|
|
37
|
+
if (err)
|
|
38
|
+
return c.json({ ok: false, error: err.msg }, err.status);
|
|
39
|
+
let body = {};
|
|
40
|
+
try {
|
|
41
|
+
body = await c.req.json();
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
body = {};
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const rec = await addAccount(agent, String(body.label || ''), String(body.token || ''));
|
|
48
|
+
return c.json({ ok: true, account: await publicAccount(agent, rec, getActiveAccountId(agent)) });
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
return c.json({ ok: false, error: e?.message || String(e) }, 400);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
app.patch('/api/agents/:agent/accounts/:id', async (c) => {
|
|
55
|
+
const agent = c.req.param('agent');
|
|
56
|
+
const id = c.req.param('id');
|
|
57
|
+
const err = agentError(agent);
|
|
58
|
+
if (err)
|
|
59
|
+
return c.json({ ok: false, error: err.msg }, err.status);
|
|
60
|
+
if (!getAccount(agent, id))
|
|
61
|
+
return c.json({ ok: false, error: 'Account not found' }, 404);
|
|
62
|
+
let body = {};
|
|
63
|
+
try {
|
|
64
|
+
body = await c.req.json();
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
body = {};
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const rec = await updateAccount(agent, id, {
|
|
71
|
+
label: typeof body.label === 'string' ? body.label : undefined,
|
|
72
|
+
token: typeof body.token === 'string' && body.token.trim() ? body.token : undefined,
|
|
73
|
+
});
|
|
74
|
+
return c.json({ ok: true, account: await publicAccount(agent, rec, getActiveAccountId(agent)) });
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
return c.json({ ok: false, error: e?.message || String(e) }, 400);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
app.delete('/api/agents/:agent/accounts/:id', async (c) => {
|
|
81
|
+
const agent = c.req.param('agent');
|
|
82
|
+
const id = c.req.param('id');
|
|
83
|
+
const err = agentError(agent);
|
|
84
|
+
if (err)
|
|
85
|
+
return c.json({ ok: false, error: err.msg }, err.status);
|
|
86
|
+
if (!(await removeAccount(agent, id)))
|
|
87
|
+
return c.json({ ok: false, error: 'Account not found' }, 404);
|
|
88
|
+
return c.json({ ok: true });
|
|
89
|
+
});
|
|
90
|
+
app.post('/api/agents/:agent/active-account', async (c) => {
|
|
91
|
+
const agent = c.req.param('agent');
|
|
92
|
+
const err = agentError(agent);
|
|
93
|
+
if (err)
|
|
94
|
+
return c.json({ ok: false, error: err.msg }, err.status);
|
|
95
|
+
let body = {};
|
|
96
|
+
try {
|
|
97
|
+
body = await c.req.json();
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
body = {};
|
|
101
|
+
}
|
|
102
|
+
const accountId = body.accountId === null ? null : (typeof body.accountId === 'string' ? body.accountId : undefined);
|
|
103
|
+
if (accountId === undefined)
|
|
104
|
+
return c.json({ ok: false, error: 'accountId (string|null) is required' }, 400);
|
|
105
|
+
try {
|
|
106
|
+
setActiveAccount(agent, accountId);
|
|
107
|
+
return c.json({ ok: true, agent, activeAccountId: accountId });
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
return c.json({ ok: false, error: e?.message || String(e) }, 400);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
export default app;
|
package/dist/dashboard/server.js
CHANGED
|
@@ -13,6 +13,7 @@ import extensionRoutes from './routes/extensions.js';
|
|
|
13
13
|
import cliRoutes from './routes/cli.js';
|
|
14
14
|
import modelsRoutes from './routes/models.js';
|
|
15
15
|
import localModelsRoutes from './routes/local-models.js';
|
|
16
|
+
import accountsRoutes from './routes/accounts.js';
|
|
16
17
|
import { runtime } from './runtime.js';
|
|
17
18
|
import { registerProcessRuntime } from '../core/process-control.js';
|
|
18
19
|
import { mountPikichannel } from '../pikichannel/server.js';
|
|
@@ -30,6 +31,7 @@ export async function startDashboard(opts = {}) {
|
|
|
30
31
|
app.route('/', cliRoutes);
|
|
31
32
|
app.route('/', modelsRoutes);
|
|
32
33
|
app.route('/', localModelsRoutes);
|
|
34
|
+
app.route('/', accountsRoutes);
|
|
33
35
|
let pikichannel = null;
|
|
34
36
|
try {
|
|
35
37
|
pikichannel = await mountPikichannel(app);
|
|
@@ -137,7 +137,33 @@ async function handleResponses(req, res, upstreamBase, body) {
|
|
|
137
137
|
const tools = new Map();
|
|
138
138
|
let usage = null;
|
|
139
139
|
const finalItems = [];
|
|
140
|
+
// Reasoning ("thinking") from chat-only providers arrives as delta.reasoning_content
|
|
141
|
+
// (DeepSeek / 豆包) or delta.reasoning (GLM / OpenRouter). Translate it to the Responses
|
|
142
|
+
// reasoning-summary stream so codex surfaces item/reasoning/summaryTextDelta live; without
|
|
143
|
+
// this the entire thinking phase is silent and the answer looks like it appears all at once.
|
|
144
|
+
let reasoningOpen = false, reasoningDone = false, reasoningId = '', reasoningIndex = 0, reasoningText = '';
|
|
145
|
+
const openReasoning = () => {
|
|
146
|
+
if (reasoningOpen || reasoningDone)
|
|
147
|
+
return;
|
|
148
|
+
reasoningOpen = true;
|
|
149
|
+
reasoningId = genId('rs');
|
|
150
|
+
reasoningIndex = nextIndex++;
|
|
151
|
+
emit('response.output_item.added', { output_index: reasoningIndex, item: { id: reasoningId, type: 'reasoning', summary: [] } });
|
|
152
|
+
emit('response.reasoning_summary_part.added', { item_id: reasoningId, output_index: reasoningIndex, summary_index: 0, part: { type: 'summary_text', text: '' } });
|
|
153
|
+
};
|
|
154
|
+
const closeReasoning = () => {
|
|
155
|
+
if (!reasoningOpen)
|
|
156
|
+
return;
|
|
157
|
+
reasoningOpen = false;
|
|
158
|
+
reasoningDone = true;
|
|
159
|
+
emit('response.reasoning_summary_text.done', { item_id: reasoningId, output_index: reasoningIndex, summary_index: 0, text: reasoningText });
|
|
160
|
+
emit('response.reasoning_summary_part.done', { item_id: reasoningId, output_index: reasoningIndex, summary_index: 0, part: { type: 'summary_text', text: reasoningText } });
|
|
161
|
+
const item = { id: reasoningId, type: 'reasoning', summary: reasoningText ? [{ type: 'summary_text', text: reasoningText }] : [] };
|
|
162
|
+
emit('response.output_item.done', { output_index: reasoningIndex, item });
|
|
163
|
+
finalItems.push(item);
|
|
164
|
+
};
|
|
140
165
|
const openMsg = () => {
|
|
166
|
+
closeReasoning(); // a reasoning item must finish before the message item opens (ordered output)
|
|
141
167
|
if (msgOpen)
|
|
142
168
|
return;
|
|
143
169
|
msgOpen = true;
|
|
@@ -178,12 +204,22 @@ async function handleResponses(req, res, upstreamBase, body) {
|
|
|
178
204
|
if (!choice)
|
|
179
205
|
continue;
|
|
180
206
|
const delta = choice.delta || {};
|
|
207
|
+
const reasoningChunk = typeof delta.reasoning_content === 'string' ? delta.reasoning_content
|
|
208
|
+
: typeof delta.reasoning === 'string' ? delta.reasoning : '';
|
|
209
|
+
if (reasoningChunk && !msgOpen && tools.size === 0) {
|
|
210
|
+
openReasoning();
|
|
211
|
+
if (reasoningOpen) {
|
|
212
|
+
reasoningText += reasoningChunk;
|
|
213
|
+
emit('response.reasoning_summary_text.delta', { item_id: reasoningId, output_index: reasoningIndex, summary_index: 0, delta: reasoningChunk });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
181
216
|
if (typeof delta.content === 'string' && delta.content) {
|
|
182
217
|
openMsg();
|
|
183
218
|
msgText += delta.content;
|
|
184
219
|
emit('response.output_text.delta', { item_id: msgId, output_index: msgIndex, content_index: 0, delta: delta.content });
|
|
185
220
|
}
|
|
186
221
|
if (Array.isArray(delta.tool_calls)) {
|
|
222
|
+
closeReasoning(); // tool calls follow the thinking phase; close the reasoning item first
|
|
187
223
|
for (const tc of delta.tool_calls) {
|
|
188
224
|
const idx = typeof tc.index === 'number' ? tc.index : 0;
|
|
189
225
|
let st = tools.get(idx);
|
|
@@ -213,6 +249,7 @@ async function handleResponses(req, res, upstreamBase, body) {
|
|
|
213
249
|
catch (e) {
|
|
214
250
|
warn(`stream read error: ${e?.message || e}`);
|
|
215
251
|
}
|
|
252
|
+
closeReasoning(); // reasoning-only turn (no content/tools followed): finish the reasoning item
|
|
216
253
|
if (msgOpen) {
|
|
217
254
|
emit('response.output_text.done', { item_id: msgId, output_index: msgIndex, content_index: 0, text: msgText });
|
|
218
255
|
const item = { id: msgId, type: 'message', role: 'assistant', content: [{ type: 'output_text', text: msgText }] };
|
package/package.json
CHANGED
|
@@ -275,6 +275,40 @@ plugins are the composable per-capability layer on top.)
|
|
|
275
275
|
|
|
276
276
|
---
|
|
277
277
|
|
|
278
|
+
## Workspace: unified directory + session / skill / mcp management
|
|
279
|
+
|
|
280
|
+
One explicitly-configurable **top-level directory** (`createLoom({ stateDirName })`, default
|
|
281
|
+
`'pikiloom'` → `.pikiloom`) gives a consuming app the same "everything under one folder" model
|
|
282
|
+
pikiloom uses, resolved in two scopes — global (`~/.pikiloom`) and per-workspace
|
|
283
|
+
(`<workdir>/.pikiloom`). It's exposed off the `Loom`:
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
const loom = createLoom({ drivers: [new ClaudeDriver(), new CodexDriver()], stateDirName: 'pikiloom' });
|
|
287
|
+
|
|
288
|
+
// Unified, searchable session list — the kernel's MANAGED sessions (scoped per workspace by the
|
|
289
|
+
// cwd they ran in) MERGED with each agent's OWN native sessions (claude/codex read their on-disk
|
|
290
|
+
// transcripts). Global view, per-workspace view, and search — all kernel-owned:
|
|
291
|
+
await loom.sessions.list({ scope: 'workspace', workdir }); // this folder (managed + native)
|
|
292
|
+
await loom.sessions.list({ scope: 'global' }); // every workspace's managed sessions
|
|
293
|
+
await loom.sessions.search({ query: 'deploy', workdir });
|
|
294
|
+
|
|
295
|
+
// Skills registry: one canonical dir, symlinked into every agent's skills dir.
|
|
296
|
+
loom.skills.ensureLinks('workspace', workdir); // <wd>/.claude/skills + .agents/skills → <wd>/.pikiloom/skills
|
|
297
|
+
loom.skills.list({ workdir }); // installed skills (workspace + global)
|
|
298
|
+
await loom.skills.search('pdf'); // installable skills (npm)
|
|
299
|
+
|
|
300
|
+
// MCP catalog + discovery (enabling a server stays on the Plugin.tools()/ToolProvider seam):
|
|
301
|
+
loom.mcp.recommended(); // curated catalog
|
|
302
|
+
await loom.mcp.search('postgres'); // MCP registry → npm
|
|
303
|
+
|
|
304
|
+
loom.paths.skillsDir('global'); // ~/.pikiloom/skills, etc.
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
A driver opts into native discovery by implementing
|
|
308
|
+
`listNativeSessions?({ workdir, limit }): NativeSessionInfo[]` (Claude/Codex/Gemini do; the pure
|
|
309
|
+
readers are also exported as `discover{Claude,Codex,Gemini}NativeSessions`). All of this is
|
|
310
|
+
node-builtins-only and additive — every existing port/default is unchanged.
|
|
311
|
+
|
|
278
312
|
## Exports
|
|
279
313
|
|
|
280
314
|
Main entry `@pikiloom/kernel` re-exports everything. Subpaths: `@pikiloom/kernel/drivers`,
|
|
@@ -284,6 +318,7 @@ Main entry `@pikiloom/kernel` re-exports everything. Subpaths: `@pikiloom/kernel
|
|
|
284
318
|
- Drivers: `EchoDriver`, `ClaudeDriver`, `CodexDriver`, `GeminiDriver`, `HermesDriver`
|
|
285
319
|
- Surfaces: `WebSurface`, `CliSurface`
|
|
286
320
|
- Ports/defaults: `FsSessionStore`, `NullModelResolver`, `NoopToolProvider`, `PassthroughSystemPromptBuilder`, `AutoCancelInteractionHandler`, `DeferToTerminalInteractionHandler`, `NoopCatalog`, `defaultBaseDir`
|
|
321
|
+
- Workspace: `resolveLoomPaths`, `SessionsManager`, `SkillsManager`, `McpRegistry`, `ensureDirSymlink`, `discover{Claude,Codex,Gemini}NativeSessions` + types `LoomPaths`, `LoomScope`, `ManagedSessionInfo`, `NativeSessionInfo`, `SkillInfo`, `McpCatalogEntry`
|
|
287
322
|
- Protocol: `UniversalSnapshot`, `diffSnapshot`, `applySnapshotPatch`, `emptySnapshot`, `PROTOCOL_VERSION`, all wire/`Client*`/`Server*` message types
|
|
288
323
|
- Types: `AgentDriver`, `AgentTurnInput`, `DriverContext`, `DriverEvent`, `DriverResult`, `LoomIO`, `PromptInput`, `Surface`, `Plugin`, `SpawnContribution`, `SessionStore`, `ModelResolver`, `ToolProvider`, `SystemPromptBuilder`, `InteractionHandler`, `Catalog`, …
|
|
289
324
|
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Whether this agent's local accounts are switched by injecting a token. */
|
|
2
|
+
export declare function accountTokenSupported(agent: string): boolean;
|
|
3
|
+
/** The env-var name that overrides this agent's subscription identity, or null. */
|
|
4
|
+
export declare function accountTokenEnvVar(agent: string): string | null;
|
|
5
|
+
/** Env to merge at spawn so the agent runs under the given account token. */
|
|
6
|
+
export declare function accountTokenEnv(agent: string, token: string): Record<string, string>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Multi-account capability (token form): switch between several local subscription accounts
|
|
2
|
+
// of a coding-agent CLI by injecting a long-lived auth token at spawn time. The CLI keeps
|
|
3
|
+
// using its normal single config home (~/.claude etc.) — only the *identity* changes.
|
|
4
|
+
//
|
|
5
|
+
// claude / claude-tui -> CLAUDE_CODE_OAUTH_TOKEN=<token from `claude setup-token`>
|
|
6
|
+
//
|
|
7
|
+
// This replaces the earlier per-account config-directory approach: on macOS claude stores
|
|
8
|
+
// its credential in one shared OS-keychain item (not per CLAUDE_CONFIG_DIR), so the token is
|
|
9
|
+
// what actually isolates an account. codex has no equivalent token, so it is not supported
|
|
10
|
+
// here yet (single account only).
|
|
11
|
+
//
|
|
12
|
+
// Dependency-free (no imports) so any @pikiloom/kernel consumer inherits account switching.
|
|
13
|
+
const ACCOUNT_TOKEN_ENV = {
|
|
14
|
+
claude: 'CLAUDE_CODE_OAUTH_TOKEN',
|
|
15
|
+
'claude-tui': 'CLAUDE_CODE_OAUTH_TOKEN',
|
|
16
|
+
};
|
|
17
|
+
/** Whether this agent's local accounts are switched by injecting a token. */
|
|
18
|
+
export function accountTokenSupported(agent) {
|
|
19
|
+
return Object.prototype.hasOwnProperty.call(ACCOUNT_TOKEN_ENV, agent);
|
|
20
|
+
}
|
|
21
|
+
/** The env-var name that overrides this agent's subscription identity, or null. */
|
|
22
|
+
export function accountTokenEnvVar(agent) {
|
|
23
|
+
return ACCOUNT_TOKEN_ENV[agent] ?? null;
|
|
24
|
+
}
|
|
25
|
+
/** Env to merge at spawn so the agent runs under the given account token. */
|
|
26
|
+
export function accountTokenEnv(agent, token) {
|
|
27
|
+
const key = accountTokenEnvVar(agent);
|
|
28
|
+
return key && token ? { [key]: token } : {};
|
|
29
|
+
}
|
|
@@ -82,6 +82,17 @@ export interface TuiSpec {
|
|
|
82
82
|
cwd: string;
|
|
83
83
|
env?: Record<string, string>;
|
|
84
84
|
}
|
|
85
|
+
export interface NativeSessionInfo {
|
|
86
|
+
sessionId: string;
|
|
87
|
+
title: string | null;
|
|
88
|
+
preview: string | null;
|
|
89
|
+
cwd: string | null;
|
|
90
|
+
model: string | null;
|
|
91
|
+
createdAt: string | null;
|
|
92
|
+
updatedAt: string | null;
|
|
93
|
+
running: boolean;
|
|
94
|
+
messageCount?: number | null;
|
|
95
|
+
}
|
|
85
96
|
export interface AgentDriver {
|
|
86
97
|
readonly id: string;
|
|
87
98
|
readonly capabilities?: {
|
|
@@ -92,4 +103,8 @@ export interface AgentDriver {
|
|
|
92
103
|
};
|
|
93
104
|
run(input: AgentTurnInput, ctx: DriverContext): Promise<DriverResult>;
|
|
94
105
|
tui?(input: TuiInput): TuiSpec;
|
|
106
|
+
listNativeSessions?(opts: {
|
|
107
|
+
workdir: string;
|
|
108
|
+
limit?: number;
|
|
109
|
+
}): NativeSessionInfo[] | Promise<NativeSessionInfo[]>;
|
|
95
110
|
}
|
|
@@ -5,9 +5,11 @@ export interface CoreSessionRecord {
|
|
|
5
5
|
sessionId: string;
|
|
6
6
|
nativeSessionId?: string | null;
|
|
7
7
|
workspacePath: string;
|
|
8
|
+
workdir?: string | null;
|
|
8
9
|
createdAt: string;
|
|
9
10
|
updatedAt: string;
|
|
10
11
|
title?: string | null;
|
|
12
|
+
preview?: string | null;
|
|
11
13
|
model?: string | null;
|
|
12
14
|
effort?: string | null;
|
|
13
15
|
runState?: 'running' | 'completed' | 'incomplete';
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { AgentDriver, AgentTurnInput, DriverContext, DriverResult, DriverEvent, McpServerSpec } from '../contracts/driver.js';
|
|
2
|
+
export interface AcpDriverConfig {
|
|
3
|
+
id: string;
|
|
4
|
+
command: string;
|
|
5
|
+
args?: string[];
|
|
6
|
+
env?: Record<string, string>;
|
|
7
|
+
fsAccess?: boolean;
|
|
8
|
+
permissionFallback?: 'allow' | 'reject' | 'cancel';
|
|
9
|
+
protocolVersion?: number;
|
|
10
|
+
promptTimeoutMs?: number;
|
|
11
|
+
capabilities?: AgentDriver['capabilities'];
|
|
12
|
+
}
|
|
13
|
+
export declare function toAcpMcpServers(servers?: McpServerSpec[]): any[];
|
|
14
|
+
export declare function buildAcpPromptBlocks(prompt: string, attachments: string[]): any[];
|
|
15
|
+
export declare function applyAcpUpdate(update: any, s: any, tools: Set<string>, emit: (e: DriverEvent) => void): void;
|
|
16
|
+
export declare class AcpDriver implements AgentDriver {
|
|
17
|
+
readonly id: string;
|
|
18
|
+
readonly capabilities: NonNullable<AgentDriver['capabilities']>;
|
|
19
|
+
protected readonly cfg: Required<Omit<AcpDriverConfig, 'capabilities' | 'env'>> & Pick<AcpDriverConfig, 'env' | 'capabilities'>;
|
|
20
|
+
constructor(config: AcpDriverConfig);
|
|
21
|
+
run(input: AgentTurnInput, ctx: DriverContext): Promise<DriverResult>;
|
|
22
|
+
private resolvePermission;
|
|
23
|
+
private startupError;
|
|
24
|
+
}
|