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
@@ -1 +1 @@
1
- import{j as s}from"./react-vendor-C7Sl8SE7.js";import{N as t,m as i,S as a}from"./index-qKexKdJl.js";function o({primary:e,secondary:l,tertiary:n}){return s.jsxs("div",{className:"flex flex-col gap-2 pt-1 sm:flex-row sm:flex-wrap sm:items-center sm:justify-between",children:[s.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[e&&s.jsxs(i,{tone:"primary",onClick:e.onClick,disabled:e.disabled,children:[e.loading&&s.jsx(a,{}),e.label]}),l&&s.jsx(i,{tone:"secondary",onClick:l.onClick,disabled:l.disabled,children:l.label})]}),n&&s.jsx("div",{className:"text-xs leading-relaxed text-fg-4",children:n})]})}function c({children:e,className:l}){return s.jsx(t,{padding:"md",elevation:"flat",className:l,children:e})}export{o as A,c as S};
1
+ import{j as s}from"./react-vendor-C7Sl8SE7.js";import{O as t,m as i,S as a}from"./index-BbplxgnQ.js";function o({primary:e,secondary:l,tertiary:n}){return s.jsxs("div",{className:"flex flex-col gap-2 pt-1 sm:flex-row sm:flex-wrap sm:items-center sm:justify-between",children:[s.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[e&&s.jsxs(i,{tone:"primary",onClick:e.onClick,disabled:e.disabled,children:[e.loading&&s.jsx(a,{}),e.label]}),l&&s.jsx(i,{tone:"secondary",onClick:l.onClick,disabled:l.disabled,children:l.label})]}),n&&s.jsx("div",{className:"text-xs leading-relaxed text-fg-4",children:n})]})}function c({children:e,className:l}){return s.jsx(t,{padding:"md",elevation:"flat",className:l,children:e})}export{o as A,c as S};
@@ -6,10 +6,10 @@
6
6
  <link rel="icon" type="image/png" href="/logo.png">
7
7
  <title>Pikiloom</title>
8
8
  <link href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=Geist+Mono:wght@400;500&display=swap" rel="stylesheet">
9
- <script type="module" crossorigin src="/assets/index-qKexKdJl.js"></script>
9
+ <script type="module" crossorigin src="/assets/index-BbplxgnQ.js"></script>
10
10
  <link rel="modulepreload" crossorigin href="/assets/react-vendor-C7Sl8SE7.js">
11
11
  <link rel="modulepreload" crossorigin href="/assets/router-DHISdpPk.js">
12
- <link rel="stylesheet" crossorigin href="/assets/index-Bthwt6K_.css">
12
+ <link rel="stylesheet" crossorigin href="/assets/index-DwmXPtDd.css">
13
13
  </head>
14
14
  <body>
15
15
  <div id="root"></div>
@@ -0,0 +1,181 @@
1
+ // Multi-account store (app layer): local subscription accounts for an agent, switched by
2
+ // injecting a long-lived auth token at spawn. Each account = a user-named token (minted via
3
+ // `claude setup-token`), stored in the credential vault (not plaintext). The agent keeps
4
+ // using its normal single config home; only CLAUDE_CODE_OAUTH_TOKEN changes.
5
+ //
6
+ // claude only for now — codex has no token-based switch (see kernel accounts.ts).
7
+ import { randomUUID } from 'node:crypto';
8
+ import { loadUserConfig, saveUserConfig } from '../core/config/user-config.js';
9
+ import { persistSecret, forgetSecret, resolveCredential, isCredentialRef } from '../core/secrets/index.js';
10
+ import { loadKernel } from './kernel-bridge.js';
11
+ import { claudeUsageForToken } from './drivers/claude.js';
12
+ import { agentWarn } from './utils.js';
13
+ // Agents whose local accounts are switched by a token (kept in sync with the kernel).
14
+ const ACCOUNT_AGENTS = new Set(['claude']);
15
+ export const MAX_ACCOUNTS_PER_AGENT = 5;
16
+ const TOKEN_RE = /^sk-ant-oat/;
17
+ export function accountAgentSupported(agent) {
18
+ return ACCOUNT_AGENTS.has(agent);
19
+ }
20
+ function getLayer() {
21
+ return loadUserConfig().accounts || {};
22
+ }
23
+ function writeLayer(layer) {
24
+ saveUserConfig({ ...loadUserConfig(), accounts: layer });
25
+ }
26
+ // Only records that carry a resolvable credential are real accounts; this also drops stale
27
+ // records from the earlier config-directory implementation (which had no credential).
28
+ function validRecords(agent) {
29
+ return (getLayer().byAgent?.[agent] || []).filter(r => r && isCredentialRef(r.credential));
30
+ }
31
+ export function listAccounts(agent) {
32
+ return validRecords(agent).slice().sort((a, b) => a.createdAt.localeCompare(b.createdAt));
33
+ }
34
+ export function getAccount(agent, id) {
35
+ return validRecords(agent).find(a => a.id === id) || null;
36
+ }
37
+ export async function addAccount(agent, label, token) {
38
+ if (!accountAgentSupported(agent))
39
+ throw new Error(`Agent ${agent} does not support multiple accounts`);
40
+ const name = (label || '').trim();
41
+ if (!name)
42
+ throw new Error('Account name is required');
43
+ const t = (token || '').trim();
44
+ if (!TOKEN_RE.test(t))
45
+ throw new Error('Expected a token starting with "sk-ant-oat" (the output of `claude setup-token`)');
46
+ const existing = listAccounts(agent);
47
+ if (existing.length >= MAX_ACCOUNTS_PER_AGENT)
48
+ throw new Error(`At most ${MAX_ACCOUNTS_PER_AGENT} accounts per agent`);
49
+ const id = randomUUID().slice(0, 8);
50
+ const credential = await persistSecret(`account/${agent}/${id}`, t);
51
+ const rec = { id, label: name, credential, createdAt: new Date().toISOString(), lastUsedAt: null };
52
+ const layer = getLayer();
53
+ const byAgent = { ...(layer.byAgent || {}) };
54
+ byAgent[agent] = [...existing, rec];
55
+ writeLayer({ ...layer, byAgent });
56
+ return rec;
57
+ }
58
+ export async function updateAccount(agent, id, patch) {
59
+ const list = listAccounts(agent);
60
+ const idx = list.findIndex(a => a.id === id);
61
+ if (idx < 0)
62
+ throw new Error(`Account not found: ${id}`);
63
+ const cur = list[idx];
64
+ let credential = cur.credential;
65
+ if (patch.token !== undefined) {
66
+ const t = patch.token.trim();
67
+ if (!TOKEN_RE.test(t))
68
+ throw new Error('Expected a token starting with "sk-ant-oat"');
69
+ credential = await persistSecret(`account/${agent}/${id}`, t);
70
+ }
71
+ const next = {
72
+ ...cur,
73
+ credential,
74
+ ...('label' in patch && patch.label !== undefined ? { label: patch.label.trim() || cur.label } : {}),
75
+ ...('lastUsedAt' in patch ? { lastUsedAt: patch.lastUsedAt ?? null } : {}),
76
+ };
77
+ list[idx] = next;
78
+ const layer = getLayer();
79
+ writeLayer({ ...layer, byAgent: { ...(layer.byAgent || {}), [agent]: list } });
80
+ return next;
81
+ }
82
+ export async function removeAccount(agent, id) {
83
+ const rec = getAccount(agent, id);
84
+ if (!rec)
85
+ return false;
86
+ const layer = getLayer();
87
+ const byAgent = { ...(layer.byAgent || {}) };
88
+ byAgent[agent] = listAccounts(agent).filter(a => a.id !== id);
89
+ const activeByAgent = { ...(layer.activeByAgent || {}) };
90
+ if (activeByAgent[agent] === id)
91
+ activeByAgent[agent] = null;
92
+ writeLayer({ ...layer, byAgent, activeByAgent });
93
+ try {
94
+ await forgetSecret(rec.credential);
95
+ }
96
+ catch { /* best-effort */ }
97
+ return true;
98
+ }
99
+ export function getActiveAccountId(agent) {
100
+ const id = getLayer().activeByAgent?.[agent] || null;
101
+ if (id && !getAccount(agent, id))
102
+ return null;
103
+ return id;
104
+ }
105
+ export function setActiveAccount(agent, id) {
106
+ if (id && !getAccount(agent, id))
107
+ throw new Error(`Account not found: ${id}`);
108
+ const layer = getLayer();
109
+ writeLayer({ ...layer, activeByAgent: { ...(layer.activeByAgent || {}), [agent]: id } });
110
+ }
111
+ /**
112
+ * Resolve the env to inject for a turn. Tri-state override (mirrors profileId):
113
+ * undefined -> use the agent's globally-active account
114
+ * null -> no account (default login)
115
+ * <id> -> that specific account
116
+ */
117
+ export async function resolveAccountEnv(agent, accountIdOverride) {
118
+ if (!accountAgentSupported(agent))
119
+ return null;
120
+ const id = accountIdOverride === undefined ? getActiveAccountId(agent) : accountIdOverride;
121
+ if (!id)
122
+ return null;
123
+ const rec = getAccount(agent, id);
124
+ if (!rec)
125
+ return null;
126
+ let token = '';
127
+ try {
128
+ token = await resolveCredential(rec.credential);
129
+ }
130
+ catch (e) {
131
+ agentWarn(`[account] could not resolve token for ${agent}/${id}: ${e?.message || e}`);
132
+ return null;
133
+ }
134
+ if (!token)
135
+ return null;
136
+ const kernel = await loadKernel();
137
+ const env = kernel.accountTokenEnv(agent, token);
138
+ return { env, accountId: id };
139
+ }
140
+ // Last-known per-account usage, kept so synchronous surfaces (the IM `/agents` view builder)
141
+ // can render usage without awaiting a probe. Filled whenever `probeAccountUsage` resolves.
142
+ const accountUsageById = new Map();
143
+ const usageKey = (agent, id) => `${agent}/${id}`;
144
+ /** Best-effort per-account usage: read the account's quota from its token (see claudeUsageForToken). */
145
+ export async function probeAccountUsage(agent, id) {
146
+ if (agent !== 'claude')
147
+ return null;
148
+ const rec = getAccount(agent, id);
149
+ if (!rec)
150
+ return null;
151
+ try {
152
+ const token = await resolveCredential(rec.credential);
153
+ const usage = token ? await claudeUsageForToken(token) : null;
154
+ accountUsageById.set(usageKey(agent, id), usage);
155
+ return usage;
156
+ }
157
+ catch {
158
+ return null;
159
+ }
160
+ }
161
+ /** Synchronous read of the last-probed usage for an account (no network), or null. */
162
+ export function getCachedAccountUsage(agent, id) {
163
+ return accountUsageById.get(usageKey(agent, id)) ?? null;
164
+ }
165
+ /** Fire-and-forget refresh of every account's usage (cached + de-duped under the hood). */
166
+ export function warmAccountUsages(agent) {
167
+ if (!accountAgentSupported(agent))
168
+ return;
169
+ for (const rec of listAccounts(agent))
170
+ void probeAccountUsage(agent, rec.id);
171
+ }
172
+ /** Compact "5h 100% · 7d 19%" summary from the cached usage, or null. */
173
+ export function accountUsageSummary(agent, id) {
174
+ const usage = getCachedAccountUsage(agent, id);
175
+ if (!usage?.ok || !usage.windows.length)
176
+ return null;
177
+ const parts = usage.windows
178
+ .filter(w => w.usedPercent != null)
179
+ .map(w => `${w.label} ${Math.round(w.usedPercent)}%`);
180
+ return parts.length ? parts.join(' · ') : null;
181
+ }
@@ -3,7 +3,7 @@ import path from 'node:path';
3
3
  import { execSync, spawn } from 'node:child_process';
4
4
  import { createInterface } from 'node:readline';
5
5
  import { registerDriver } from '../driver.js';
6
- import { Q, run, agentError, agentLog, agentWarn, buildStreamPreviewMeta, computeContext, pushRecentActivity, summarizeClaudeToolUse, summarizeClaudeToolResult, joinErrorMessages, parseTodoWriteAsPlan, previewToolCallInput, previewToolCallResult, detectClaudeApiError, isRetryableClaudeApiError, detectClaudeModelError, claudeModelErrorMessage, emitSessionIdUpdate, IMAGE_EXTS, mimeForExt, listPikiloomSessions, mergeManagedAndNativeSessions, managedRecordToSessionInfo, readTailLines, stripInjectedPrompts, sanitizeSessionUserPreviewText, SESSION_PREVIEW_IMAGE_PLACEHOLDER_RE, CLAUDE_AT_MENTION_IMAGE_RE, extractClaudeAtMentionImagePaths, attachAgentImage, applyTurnWindow, shortValue, roundPercent, modelFamily, normalizeClaudeModelId, emptyUsage, normalizeUsageStatus, collapseSkillPrompt, } from '../index.js';
6
+ import { Q, run, agentError, agentLog, agentWarn, buildStreamPreviewMeta, computeContext, pushRecentActivity, summarizeClaudeToolUse, summarizeClaudeToolResult, joinErrorMessages, parseTodoWriteAsPlan, previewToolCallInput, previewToolCallResult, detectClaudeApiError, isRetryableClaudeApiError, detectClaudeModelError, claudeModelErrorMessage, emitSessionIdUpdate, IMAGE_EXTS, mimeForExt, listPikiloomSessions, mergeManagedAndNativeSessions, managedRecordToSessionInfo, readTailLines, stripInjectedPrompts, sanitizeSessionUserPreviewText, SESSION_PREVIEW_IMAGE_PLACEHOLDER_RE, CLAUDE_AT_MENTION_IMAGE_RE, extractClaudeAtMentionImagePaths, attachAgentImage, applyTurnWindow, shortValue, roundPercent, toIsoFromEpochSeconds, modelFamily, normalizeClaudeModelId, emptyUsage, normalizeUsageStatus, collapseSkillPrompt, } from '../index.js';
7
7
  import { AGENT_STREAM_HARD_KILL_GRACE_MS, AGENT_GRACEFUL_ABORT_GRACE_MS, SESSION_RUNNING_THRESHOLD_MS } from '../../core/constants.js';
8
8
  import { terminateProcessTree } from '../../core/process-control.js';
9
9
  import { getHome, IS_MAC, encodePathAsDirName } from '../../core/platform.js';
@@ -1775,8 +1775,8 @@ function getClaudeOAuthToken() {
1775
1775
  return null;
1776
1776
  }
1777
1777
  }
1778
- function getClaudeUsageFromOAuth() {
1779
- const token = getClaudeOAuthToken();
1778
+ function getClaudeUsageFromOAuth(tokenOverride) {
1779
+ const token = tokenOverride || getClaudeOAuthToken();
1780
1780
  if (!token)
1781
1781
  return null;
1782
1782
  try {
@@ -1824,6 +1824,111 @@ function getClaudeUsageFromOAuth() {
1824
1824
  return null;
1825
1825
  }
1826
1826
  }
1827
+ // Per-account usage for a specific account's `claude setup-token`.
1828
+ //
1829
+ // IMPORTANT: setup-tokens are minted with the `user:inference` scope only — they do NOT carry
1830
+ // `user:profile`, so the read-only OAuth usage endpoint (`/api/oauth/usage`) rejects them with
1831
+ // `permission_error … scope requirement user:profile`. The native/default-login token works
1832
+ // there because it's a full login. So for account tokens we read the limit state from the
1833
+ // `anthropic-ratelimit-unified-*` *response headers* of a tiny inference call instead — those
1834
+ // headers come back on every `/v1/messages` response regardless of scope, and are exactly what
1835
+ // Claude Code itself uses to learn 5h / 7d limits. Cost is ~1 output token per probe, cached
1836
+ // for 5min per token (60s back-off on failure), with in-flight de-dup so concurrent surfaces
1837
+ // (cards + header + IM) share one probe.
1838
+ const TOKEN_USAGE_OK_TTL_MS = CLAUDE_USAGE_QUERY_TTL_MS;
1839
+ const TOKEN_USAGE_RETRY_TTL_MS = 60_000;
1840
+ const CLAUDE_USAGE_PROBE_MODEL = 'claude-haiku-4-5-20251001';
1841
+ const claudeTokenUsageCache = new Map();
1842
+ const claudeTokenUsageInflight = new Map();
1843
+ function usageFromRatelimitHeaders(h) {
1844
+ const makeWindow = (label, prefix) => {
1845
+ const raw = h.get(`anthropic-ratelimit-unified-${prefix}-utilization`);
1846
+ if (raw == null || raw === '')
1847
+ return null;
1848
+ const usedPercent = roundPercent(Number(raw) * 100);
1849
+ if (usedPercent == null)
1850
+ return null;
1851
+ const remainingPercent = Math.max(0, Math.round((100 - usedPercent) * 10) / 10);
1852
+ const resetAt = toIsoFromEpochSeconds(h.get(`anthropic-ratelimit-unified-${prefix}-reset`));
1853
+ let resetAfterSeconds = null;
1854
+ if (resetAt) {
1855
+ const resetAtMs = Date.parse(resetAt);
1856
+ if (Number.isFinite(resetAtMs))
1857
+ resetAfterSeconds = Math.max(0, Math.round((resetAtMs - Date.now()) / 1000));
1858
+ }
1859
+ return {
1860
+ label, usedPercent, remainingPercent, resetAt, resetAfterSeconds,
1861
+ status: usedPercent >= 100 ? 'limit_reached' : usedPercent >= 80 ? 'warning' : 'allowed',
1862
+ };
1863
+ };
1864
+ // Only 5h + 7d are real here. The inference headers expose an overage (Extra) on/off STATUS
1865
+ // but never a utilization number, and `/api/oauth/usage` (which returns the actual extra_usage
1866
+ // figure) is scope-blocked for setup-tokens — so we deliberately do NOT synthesize an "Extra"
1867
+ // window for token accounts. Showing a fabricated 0% misrepresents real Extra spend.
1868
+ const windows = [makeWindow('5h', '5h'), makeWindow('7d', '7d')].filter((w) => w != null);
1869
+ if (!windows.length)
1870
+ return null;
1871
+ const overallStatus = windows.some(w => w.status === 'limit_reached') ? 'limit_reached'
1872
+ : windows.some(w => w.status === 'warning') ? 'warning' : 'allowed';
1873
+ return { ok: true, agent: 'claude', source: 'ratelimit-headers', capturedAt: new Date().toISOString(), status: overallStatus, windows, error: null };
1874
+ }
1875
+ async function claudeUsageFromInferenceHeaders(token) {
1876
+ const controller = new AbortController();
1877
+ const timer = setTimeout(() => controller.abort(), 10_000);
1878
+ try {
1879
+ const res = await fetch('https://api.anthropic.com/v1/messages', {
1880
+ method: 'POST',
1881
+ headers: {
1882
+ authorization: `Bearer ${token}`,
1883
+ 'anthropic-beta': 'oauth-2025-04-20',
1884
+ 'anthropic-version': '2023-06-01',
1885
+ 'content-type': 'application/json',
1886
+ },
1887
+ body: JSON.stringify({ model: CLAUDE_USAGE_PROBE_MODEL, max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),
1888
+ signal: controller.signal,
1889
+ });
1890
+ // The unified rate-limit headers ride on every response (incl. 429), so parse them before
1891
+ // worrying about the status code. Drain the body so the socket can be reused/closed.
1892
+ const usage = usageFromRatelimitHeaders(res.headers);
1893
+ void res.text().catch(() => { });
1894
+ return usage;
1895
+ }
1896
+ catch {
1897
+ return null;
1898
+ }
1899
+ finally {
1900
+ clearTimeout(timer);
1901
+ }
1902
+ }
1903
+ export function claudeUsageForToken(token) {
1904
+ const t = String(token || '');
1905
+ if (!t)
1906
+ return Promise.resolve(null);
1907
+ const now = Date.now();
1908
+ const cached = claudeTokenUsageCache.get(t);
1909
+ if (cached) {
1910
+ const ttl = cached.ok ? TOKEN_USAGE_OK_TTL_MS : TOKEN_USAGE_RETRY_TTL_MS;
1911
+ if (now - cached.at < ttl)
1912
+ return Promise.resolve(cached.value);
1913
+ }
1914
+ const inflight = claudeTokenUsageInflight.get(t);
1915
+ if (inflight)
1916
+ return inflight;
1917
+ const p = (async () => {
1918
+ const fresh = await claudeUsageFromInferenceHeaders(t);
1919
+ const at = Date.now();
1920
+ if (fresh) {
1921
+ claudeTokenUsageCache.set(t, { value: fresh, at, ok: true });
1922
+ return fresh;
1923
+ }
1924
+ // Probe failed (network / transient): keep serving last-good, but back off retries.
1925
+ const value = claudeTokenUsageCache.get(t)?.value ?? null;
1926
+ claudeTokenUsageCache.set(t, { value, at, ok: false });
1927
+ return value;
1928
+ })().finally(() => claudeTokenUsageInflight.delete(t));
1929
+ claudeTokenUsageInflight.set(t, p);
1930
+ return p;
1931
+ }
1827
1932
  function getClaudeUsageFromTelemetry(home, model) {
1828
1933
  const telemetryRoot = path.join(home, '.claude', 'telemetry');
1829
1934
  if (!fs.existsSync(telemetryRoot))
@@ -15,8 +15,44 @@ import { humanizeCodexError } from './drivers/codex.js';
15
15
  // Tests always run legacy (the unit suite asserts legacy driver behavior). The bridge
16
16
  // re-applies app-level parity the pure kernel must not own (claude jsonl entrypoint, codex humanize).
17
17
  const KERNEL_AGENTS = new Set(['claude', 'codex', 'gemini', 'hermes']);
18
+ // Generic ACP agents the kernel can drive via @pikiloom/kernel's AcpDriver. Hermes is its
19
+ // own built-in preset; this registry is for ANY other ACP CLI (OpenCode, Zed, …). Extend
20
+ // without a code edit via PIKILOOM_ACP_AGENTS='{"zed":{"command":"zed","args":["--acp"]}}'.
21
+ const DEFAULT_ACP_AGENTS = {
22
+ opencode: { command: 'opencode', args: ['acp'] },
23
+ };
24
+ export function acpAgentConfig(agent) {
25
+ if (DEFAULT_ACP_AGENTS[agent])
26
+ return DEFAULT_ACP_AGENTS[agent];
27
+ try {
28
+ const extra = process.env.PIKILOOM_ACP_AGENTS ? JSON.parse(process.env.PIKILOOM_ACP_AGENTS) : null;
29
+ const c = extra?.[agent];
30
+ if (c && (c.command || agent))
31
+ return { command: String(c.command || agent), args: Array.isArray(c.args) ? c.args.map(String) : ['acp'] };
32
+ }
33
+ catch { /* malformed env → ignore */ }
34
+ return null;
35
+ }
36
+ // pikiloom's mcpServers (Record) -> kernel McpServerSpec[] for ACP session/new forwarding.
37
+ function mcpRecordToKernelSpecs(servers) {
38
+ if (!servers)
39
+ return [];
40
+ const out = [];
41
+ for (const [name, cfg] of Object.entries(servers)) {
42
+ if (!cfg || typeof cfg !== 'object')
43
+ continue;
44
+ const type = String(cfg.type || '').toLowerCase();
45
+ if ((type === 'http' || type === 'sse') && cfg.url) {
46
+ out.push({ name, type: 'http', url: String(cfg.url), headers: cfg.headers || {} });
47
+ continue;
48
+ }
49
+ if (cfg.command)
50
+ out.push({ name, type: 'stdio', command: String(cfg.command), args: Array.isArray(cfg.args) ? cfg.args.map(String) : [], env: cfg.env || {} });
51
+ }
52
+ return out;
53
+ }
18
54
  export function shouldUseKernelPipeline(agent) {
19
- if (!KERNEL_AGENTS.has(agent))
55
+ if (!KERNEL_AGENTS.has(agent) && !acpAgentConfig(agent))
20
56
  return false;
21
57
  if (process.env.VITEST || process.env.NODE_ENV === 'test')
22
58
  return false; // tests assert legacy
@@ -41,7 +77,15 @@ function buildKernelDriver(kernel, opts) {
41
77
  effort: opts.thinkingEffort,
42
78
  env: opts.extraEnv,
43
79
  mcpConfigPath: opts.mcpConfigPath ?? null,
80
+ // Consumed by the ACP path (Hermes + generic AcpDriver) for session/new forwarding;
81
+ // ignored by claude/codex/gemini (they carry MCP via config file / native registration).
82
+ extraMcpServers: mcpRecordToKernelSpecs(opts.mcpServers),
44
83
  };
84
+ // Any registered generic ACP agent (OpenCode, …) -> the kernel's AcpDriver.
85
+ const acp = acpAgentConfig(opts.agent);
86
+ if (acp) {
87
+ return { driver: new kernel.AcpDriver({ id: opts.agent, command: acp.command, args: acp.args }), input: { ...common, model: opts.model ?? null } };
88
+ }
45
89
  switch (opts.agent) {
46
90
  case 'codex': {
47
91
  // codexExtraArgs is a flattened ['-c','k=v','-c','k=v',...]; extract the k=v values
@@ -184,6 +184,7 @@ function normalizeSessionRecord(raw, workdir) {
184
184
  thinkingEffort: typeof raw?.thinkingEffort === 'string' && raw.thinkingEffort.trim() ? raw.thinkingEffort.trim() : null,
185
185
  workflowEnabled: typeof raw?.workflowEnabled === 'boolean' ? raw.workflowEnabled : null,
186
186
  profileId: typeof raw?.profileId === 'string' && raw.profileId.trim() ? raw.profileId.trim() : null,
187
+ accountId: typeof raw?.accountId === 'string' && raw.accountId.trim() ? raw.accountId.trim() : null,
187
188
  stagedFiles: Array.isArray(raw?.stagedFiles) ? dedupeStrings(raw.stagedFiles.filter((v) => typeof v === 'string')) : [],
188
189
  lastUserAttachments: Array.isArray(raw?.lastUserAttachments)
189
190
  ? dedupeStrings(raw.lastUserAttachments.filter((v) => typeof v === 'string'))
@@ -327,7 +328,7 @@ function writeSessionMeta(record) {
327
328
  workspacePath: record.workspacePath,
328
329
  threadId: record.threadId,
329
330
  createdAt: record.createdAt, updatedAt: record.updatedAt,
330
- title: record.title, model: record.model, thinkingEffort: record.thinkingEffort, workflowEnabled: record.workflowEnabled, profileId: record.profileId, stagedFiles: record.stagedFiles,
331
+ title: record.title, model: record.model, thinkingEffort: record.thinkingEffort, workflowEnabled: record.workflowEnabled, profileId: record.profileId, accountId: record.accountId ?? null, stagedFiles: record.stagedFiles,
331
332
  runState: record.runState, runDetail: record.runDetail, runUpdatedAt: record.runUpdatedAt,
332
333
  runPid: record.runPid,
333
334
  classification: record.classification,
@@ -563,7 +564,7 @@ export function ensureSessionWorkspace(opts) {
563
564
  workspacePath: sessionWorkspacePath(workdir, opts.agent, sessionId),
564
565
  threadId,
565
566
  createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
566
- title: summarizePromptTitle(opts.title) || null, model: null, thinkingEffort: null, workflowEnabled: null, profileId: null, stagedFiles: [], lastUserAttachments: [],
567
+ title: summarizePromptTitle(opts.title) || null, model: null, thinkingEffort: null, workflowEnabled: null, profileId: null, accountId: null, stagedFiles: [], lastUserAttachments: [],
567
568
  runState: 'completed', runDetail: null, runUpdatedAt: new Date().toISOString(),
568
569
  runPid: null,
569
570
  classification: null, userStatus: null, userNote: null,
@@ -597,6 +598,7 @@ export function managedRecordToSessionInfo(record) {
597
598
  thinkingEffort: record.thinkingEffort,
598
599
  workflowEnabled: record.workflowEnabled ?? null,
599
600
  profileId: record.profileId ?? null,
601
+ accountId: record.accountId ?? null,
600
602
  createdAt: record.createdAt,
601
603
  title,
602
604
  running: record.runState === 'running',
@@ -680,6 +682,7 @@ export function getSessionStoredConfig(workdir, agent, sessionId) {
680
682
  thinkingEffort: record?.thinkingEffort ?? null,
681
683
  workflowEnabled: record?.workflowEnabled ?? null,
682
684
  profileId: record?.profileId ?? null,
685
+ accountId: record?.accountId ?? null,
683
686
  };
684
687
  }
685
688
  export function ensureManagedSession(opts) {
@@ -774,6 +777,7 @@ export function mergeManagedAndNativeSessions(managedSessions, nativeSessions) {
774
777
  thinkingEffort: managed.thinkingEffort ?? native.thinkingEffort ?? null,
775
778
  workflowEnabled: managed.workflowEnabled ?? native.workflowEnabled ?? null,
776
779
  profileId: managed.profileId ?? native.profileId ?? null,
780
+ accountId: managed.accountId ?? native.accountId ?? null,
777
781
  createdAt: native.createdAt || managed.createdAt,
778
782
  classification: managed.classification ?? native.classification ?? null,
779
783
  userStatus: managed.userStatus ?? native.userStatus ?? null,
@@ -13,6 +13,7 @@ import { saveSessionRecord, setSessionRunState, applySessionRunResult, ensureSes
13
13
  import { clearAwaitResume } from './await-resume.js';
14
14
  import { collapseSkillPrompt } from './skills.js';
15
15
  import { shouldUseKernelPipeline, kernelStream } from './kernel-bridge.js';
16
+ import { accountAgentSupported, resolveAccountEnv, updateAccount } from './accounts.js';
16
17
  function trimSessionText(value, max = 24_000) {
17
18
  const text = typeof value === 'string' ? value.trim() : '';
18
19
  if (!text)
@@ -531,6 +532,29 @@ export async function doStream(opts) {
531
532
  catch (e) {
532
533
  agentWarn(`[byok] failed to apply Profile injection: ${e?.message || e}`);
533
534
  }
535
+ // Multi-account (native subscription): when NO BYOK profile is bound, route the turn
536
+ // through the agent's active local account by injecting its isolated config dir
537
+ // (CLAUDE_CONFIG_DIR / CODEX_HOME). A BYOK profile owns the credential, so it takes over
538
+ // and account injection is skipped — the two axes are mutually exclusive per turn.
539
+ try {
540
+ if (accountAgentSupported(prepared.agent)) {
541
+ const profileBound = prepared.profileId === undefined
542
+ ? !!getActiveProfile(prepared.agent)
543
+ : !!(prepared.profileId && getProfile(prepared.profileId));
544
+ if (!profileBound) {
545
+ const resolved = await resolveAccountEnv(prepared.agent, prepared.accountId);
546
+ if (resolved) {
547
+ prepared.extraEnv = { ...(prepared.extraEnv || {}), ...resolved.env };
548
+ prepared.accountId = resolved.accountId;
549
+ agentLog(`[account] ${prepared.agent} turn → account=${resolved.accountId} (${Object.keys(resolved.env).join('+')})`);
550
+ void updateAccount(prepared.agent, resolved.accountId, { lastUsedAt: new Date().toISOString() }).catch(() => { });
551
+ }
552
+ }
553
+ }
554
+ }
555
+ catch (e) {
556
+ agentWarn(`[account] injection failed: ${e?.message || e}`);
557
+ }
534
558
  try {
535
559
  if (prepared.thinkingEffort) {
536
560
  session.record.thinkingEffort = prepared.thinkingEffort.trim().toLowerCase() || session.record.thinkingEffort;
@@ -548,6 +572,8 @@ export async function doStream(opts) {
548
572
  session.record.model = turnModel;
549
573
  if (prepared.profileId !== undefined)
550
574
  session.record.profileId = prepared.profileId || null;
575
+ if (prepared.accountId !== undefined)
576
+ session.record.accountId = prepared.accountId || null;
551
577
  saveSessionRecord(opts.workdir, session.record);
552
578
  }
553
579
  catch (e) {
package/dist/bot/bot.js CHANGED
@@ -2,7 +2,7 @@ import os from 'node:os';
2
2
  import path from 'node:path';
3
3
  import { execSync, spawn } from 'node:child_process';
4
4
  import { getActiveUserConfig, loadWorkspaces, onUserConfigChange, resolveUserWorkdir, setUserWorkdir, updateUserConfig } from '../core/config/user-config.js';
5
- import { doStream, ensureManagedSession, findManagedThreadSession, getSessionStoredConfig, getUsage, initializeProjectSkills, listAgents, resolveAgentModels, resolveDefaultAgent, listSkills, stageSessionFiles, reconcileOrphanedRunningSessions, getAgentBoundModelId, setAgentBoundModelId, collapseSkillPrompt, readGoal, accountTurn, shouldContinueAfterTurn, renderContinuationPrompt, renderBudgetLimitPrompt, bumpContinuationCount, pauseGoal, resumeGoal, setGoal as setGoalState, clearGoal as clearGoalState, setCodexGoal, getCodexGoal, clearCodexGoal, pauseCodexGoal, resumeCodexGoal, getClaudeNativeGoal, buildClaudeSetGoalPrompt, buildClaudeClearGoalPrompt, deliverArtifact, attachmentUrl, isPendingSessionId, } from '../agent/index.js';
5
+ import { doStream, ensureManagedSession, findManagedThreadSession, getSessionStoredConfig, getUsage, initializeProjectSkills, listAgents, resolveAgentModels, resolveDefaultAgent, listSkills, stageSessionFiles, reconcileOrphanedRunningSessions, getAgentBoundModelId, setAgentBoundModelId, collapseSkillPrompt, readGoal, accountTurn, shouldContinueAfterTurn, renderContinuationPrompt, renderBudgetLimitPrompt, bumpContinuationCount, pauseGoal, resumeGoal, setGoal as setGoalState, clearGoal as clearGoalState, setCodexGoal, getCodexGoal, clearCodexGoal, pauseCodexGoal, resumeCodexGoal, getClaudeNativeGoal, buildClaudeSetGoalPrompt, buildClaudeClearGoalPrompt, deliverArtifact, attachmentUrl, attachAgentImage, rewriteAttachmentBlocksForTransport, isPendingSessionId, } from '../agent/index.js';
6
6
  import { compactForHandover, describeHandoverRef } from '../agent/handover.js';
7
7
  import { getActiveProfileId, getActiveProfile, setActiveProfile } from '../model/index.js';
8
8
  import { querySessions, querySessionTail, updateSession, } from './session-hub.js';
@@ -160,16 +160,48 @@ export class Bot {
160
160
  const snap = this.streamSnapshots.get(this.resolveSessionKey(sessionKey));
161
161
  return snap ? this.enrichSnapshot(snap) : null;
162
162
  }
163
+ taskAttachmentBlocks(task) {
164
+ if (!task?.attachments?.length)
165
+ return [];
166
+ const session = this.getSessionRuntimeByKey(task.sessionKey, { allowAnyWorkdir: true });
167
+ const sessionId = session?.sessionId || task.sessionKey.slice(task.agent.length + 1);
168
+ if (!sessionId)
169
+ return [];
170
+ const blocks = task.attachments
171
+ .map(imagePath => attachAgentImage({ imagePath, inlineThresholdBytes: 0 }))
172
+ .filter((block) => !!block);
173
+ return blocks.length
174
+ ? rewriteAttachmentBlocksForTransport(blocks, { agent: task.agent, sessionId })
175
+ : [];
176
+ }
177
+ taskPreview(taskId) {
178
+ const task = this.activeTasks.get(taskId) || null;
179
+ const raw = task?.prompt || '';
180
+ const attachments = this.taskAttachmentBlocks(task);
181
+ return {
182
+ taskId,
183
+ prompt: collapseSkillPrompt(raw) ?? raw,
184
+ ...(attachments.length ? { attachments } : {}),
185
+ };
186
+ }
163
187
  enrichSnapshot(snap) {
164
188
  let next = snap;
165
- const runningPrompt = next.taskId ? this.activeTasks.get(next.taskId)?.prompt : '';
166
- if (runningPrompt)
167
- next = { ...next, question: collapseSkillPrompt(runningPrompt) ?? runningPrompt };
168
- if (next.queuedTaskIds?.length) {
169
- const queuedTasks = next.queuedTaskIds.map(taskId => {
170
- const raw = this.activeTasks.get(taskId)?.prompt || '';
171
- return { taskId, prompt: collapseSkillPrompt(raw) ?? raw };
172
- });
189
+ const runningTask = next.taskId ? this.activeTasks.get(next.taskId) : null;
190
+ const runningPrompt = runningTask?.prompt || '';
191
+ if (runningPrompt) {
192
+ const questionBlocks = this.taskAttachmentBlocks(runningTask);
193
+ next = {
194
+ ...next,
195
+ question: collapseSkillPrompt(runningPrompt) ?? runningPrompt,
196
+ ...(questionBlocks.length ? { questionBlocks } : {}),
197
+ };
198
+ }
199
+ const queuedIds = [
200
+ ...(next.phase === 'queued' && next.taskId ? [next.taskId] : []),
201
+ ...(next.queuedTaskIds || []),
202
+ ].filter((taskId, index, all) => taskId && all.indexOf(taskId) === index);
203
+ if (queuedIds.length) {
204
+ const queuedTasks = queuedIds.map(taskId => this.taskPreview(taskId));
173
205
  next = { ...next, queuedTasks };
174
206
  }
175
207
  if (next.interactions?.length) {
@@ -234,13 +266,16 @@ export class Bot {
234
266
  switch (event.type) {
235
267
  case 'queued': {
236
268
  const existing = this.streamSnapshots.get(sessionKey);
237
- if (existing && (existing.phase === 'streaming' || existing.phase === 'done')) {
269
+ if (existing && existing.phase === 'streaming') {
238
270
  const list = existing.queuedTaskIds ? [...existing.queuedTaskIds] : [];
239
271
  if (existing.taskId !== event.taskId && !list.includes(event.taskId))
240
272
  list.push(event.taskId);
241
273
  existing.queuedTaskIds = list.length ? list : undefined;
242
274
  existing.updatedAt = now;
243
275
  }
276
+ else if (existing && existing.phase === 'done') {
277
+ this.streamSnapshots.set(sessionKey, { phase: 'queued', taskId: event.taskId, updatedAt: now });
278
+ }
244
279
  else if (existing && existing.phase === 'queued') {
245
280
  const list = existing.queuedTaskIds ? [...existing.queuedTaskIds] : [];
246
281
  if (existing.taskId !== event.taskId && !list.includes(event.taskId))