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.
Files changed (68) hide show
  1. package/dashboard/dist/assets/AgentTab-B9z4W2mh.js +1 -0
  2. package/dashboard/dist/assets/{ConnectionModal-g_6IHAWE.js → ConnectionModal-2bL7sFKk.js} +1 -1
  3. package/dashboard/dist/assets/{DirBrowser-DVe6q12N.js → DirBrowser-CA4k8E_A.js} +1 -1
  4. package/dashboard/dist/assets/{ExtensionsTab-GqxD61Pp.js → ExtensionsTab-CRjTmFRl.js} +1 -1
  5. package/dashboard/dist/assets/{IMAccessTab-DIqTzZJ9.js → IMAccessTab-DDb7rsHe.js} +1 -1
  6. package/dashboard/dist/assets/{Modal-CIYrMwm0.js → Modal-DDiBG4kb.js} +1 -1
  7. package/dashboard/dist/assets/{Modals-B-Afu2rD.js → Modals-CsUQjpxn.js} +1 -1
  8. package/dashboard/dist/assets/{Select-B1UL27gw.js → Select-CG_7h_Tz.js} +1 -1
  9. package/dashboard/dist/assets/SessionPanel-B5964GUj.js +1 -0
  10. package/dashboard/dist/assets/{SystemTab-Bv1uT5-h.js → SystemTab-D7r1PsC9.js} +1 -1
  11. package/dashboard/dist/assets/index-BbplxgnQ.js +3 -0
  12. package/dashboard/dist/assets/index-CULTTvtY.js +23 -0
  13. package/dashboard/dist/assets/index-DwmXPtDd.css +1 -0
  14. package/dashboard/dist/assets/{shared-624PaUfm.js → shared-Bv8WvQSo.js} +1 -1
  15. package/dashboard/dist/index.html +2 -2
  16. package/dist/agent/accounts.js +181 -0
  17. package/dist/agent/drivers/claude.js +108 -3
  18. package/dist/agent/kernel-bridge.js +45 -1
  19. package/dist/agent/session.js +6 -2
  20. package/dist/agent/stream.js +26 -0
  21. package/dist/bot/bot.js +45 -10
  22. package/dist/bot/command-ui.js +69 -4
  23. package/dist/bot/render-shared.js +2 -1
  24. package/dist/cli/kernel-app.js +4 -2
  25. package/dist/core/config/runtime-config.js +14 -22
  26. package/dist/dashboard/routes/accounts.js +113 -0
  27. package/dist/dashboard/server.js +2 -0
  28. package/dist/model/responses-bridge.js +37 -0
  29. package/package.json +1 -1
  30. package/packages/kernel/README.md +35 -0
  31. package/packages/kernel/dist/accounts.d.ts +6 -0
  32. package/packages/kernel/dist/accounts.js +29 -0
  33. package/packages/kernel/dist/contracts/driver.d.ts +15 -0
  34. package/packages/kernel/dist/contracts/ports.d.ts +2 -0
  35. package/packages/kernel/dist/drivers/acp.d.ts +24 -0
  36. package/packages/kernel/dist/drivers/acp.js +477 -0
  37. package/packages/kernel/dist/drivers/claude.d.ts +5 -1
  38. package/packages/kernel/dist/drivers/claude.js +4 -0
  39. package/packages/kernel/dist/drivers/codex.d.ts +5 -1
  40. package/packages/kernel/dist/drivers/codex.js +24 -12
  41. package/packages/kernel/dist/drivers/gemini.d.ts +5 -1
  42. package/packages/kernel/dist/drivers/gemini.js +4 -0
  43. package/packages/kernel/dist/drivers/hermes.d.ts +3 -12
  44. package/packages/kernel/dist/drivers/hermes.js +8 -191
  45. package/packages/kernel/dist/drivers/index.d.ts +1 -0
  46. package/packages/kernel/dist/drivers/index.js +1 -0
  47. package/packages/kernel/dist/index.d.ts +4 -1
  48. package/packages/kernel/dist/index.js +5 -0
  49. package/packages/kernel/dist/ports/defaults.js +7 -1
  50. package/packages/kernel/dist/runtime/loom.d.ts +10 -0
  51. package/packages/kernel/dist/runtime/loom.js +18 -2
  52. package/packages/kernel/dist/workspace/index.d.ts +5 -0
  53. package/packages/kernel/dist/workspace/index.js +7 -0
  54. package/packages/kernel/dist/workspace/mcp.d.ts +44 -0
  55. package/packages/kernel/dist/workspace/mcp.js +82 -0
  56. package/packages/kernel/dist/workspace/native.d.ts +14 -0
  57. package/packages/kernel/dist/workspace/native.js +340 -0
  58. package/packages/kernel/dist/workspace/paths.d.ts +33 -0
  59. package/packages/kernel/dist/workspace/paths.js +40 -0
  60. package/packages/kernel/dist/workspace/sessions.d.ts +49 -0
  61. package/packages/kernel/dist/workspace/sessions.js +130 -0
  62. package/packages/kernel/dist/workspace/skills.d.ts +44 -0
  63. package/packages/kernel/dist/workspace/skills.js +173 -0
  64. package/dashboard/dist/assets/AgentTab-Cj7sekcS.js +0 -1
  65. package/dashboard/dist/assets/SessionPanel-BHuG5hxj.js +0 -1
  66. package/dashboard/dist/assets/index-BOEW0RIx.js +0 -23
  67. package/dashboard/dist/assets/index-Bthwt6K_.css +0 -1
  68. package/dashboard/dist/assets/index-qKexKdJl.js +0 -3
@@ -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: actions.length ? 'Tap an agent to switch.' : undefined,
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
- if (chat.agent === action.agent)
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}%`);
@@ -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
- drivers: [new EchoDriver(), new ClaudeDriver(), new CodexDriver(), new GeminiDriver(), new HermesDriver()],
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
- { id: 'low', label: 'Low' },
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;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pikiloom",
3
- "version": "0.4.40",
3
+ "version": "0.4.42",
4
4
  "description": "Put the world's smartest AI agents in your pocket. Command local Claude & Gemini via IM. | 让最好用的 IM 变成你电脑上的顶级 Agent 控制台",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
+ }