viberoom 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/NOTICE +12 -0
  2. package/README.md +119 -96
  3. package/dist/hub.js +18 -0
  4. package/dist/launcher.js +22 -0
  5. package/dist/main.js +4 -3
  6. package/dist/persona.js +2 -2
  7. package/dist/recipes.js +37 -14
  8. package/dist/room.js +3 -2
  9. package/dist/server.js +16 -3
  10. package/package.json +8 -5
  11. package/scripts/vendor-acp.mjs +85 -0
  12. package/ui/app.css +7 -0
  13. package/ui/app.js +116 -26
  14. package/ui/index.html +2 -0
  15. package/vendor/acp/claude-agent-acp/LICENSE +191 -0
  16. package/vendor/acp/claude-agent-acp/dist/acp-agent.js +7694 -0
  17. package/vendor/acp/claude-agent-acp/dist/acp-subagents.js +13 -0
  18. package/vendor/acp/claude-agent-acp/dist/air-extension.js +63 -0
  19. package/vendor/acp/claude-agent-acp/dist/async-tasks.js +613 -0
  20. package/vendor/acp/claude-agent-acp/dist/clear-context-coordinator.js +80 -0
  21. package/vendor/acp/claude-agent-acp/dist/elicitation.js +304 -0
  22. package/vendor/acp/claude-agent-acp/dist/exit-plan.js +154 -0
  23. package/vendor/acp/claude-agent-acp/dist/file-change-audit.js +350 -0
  24. package/vendor/acp/claude-agent-acp/dist/fork-session.js +41 -0
  25. package/vendor/acp/claude-agent-acp/dist/goal-extension.js +50 -0
  26. package/vendor/acp/claude-agent-acp/dist/index.js +98 -0
  27. package/vendor/acp/claude-agent-acp/dist/lib.js +5 -0
  28. package/vendor/acp/claude-agent-acp/dist/native-subagents.js +422 -0
  29. package/vendor/acp/claude-agent-acp/dist/permissions/effects.js +166 -0
  30. package/vendor/acp/claude-agent-acp/dist/permissions/modes.js +41 -0
  31. package/vendor/acp/claude-agent-acp/dist/permissions/normalization.js +100 -0
  32. package/vendor/acp/claude-agent-acp/dist/permissions/options/filesystem.js +124 -0
  33. package/vendor/acp/claude-agent-acp/dist/permissions/options/shared.js +64 -0
  34. package/vendor/acp/claude-agent-acp/dist/permissions/options/shell.js +100 -0
  35. package/vendor/acp/claude-agent-acp/dist/permissions/options/tools.js +135 -0
  36. package/vendor/acp/claude-agent-acp/dist/permissions/options.js +60 -0
  37. package/vendor/acp/claude-agent-acp/dist/permissions/presentation.js +83 -0
  38. package/vendor/acp/claude-agent-acp/dist/permissions/response.js +19 -0
  39. package/vendor/acp/claude-agent-acp/dist/session-config-ids.js +4 -0
  40. package/vendor/acp/claude-agent-acp/dist/session-failure-extension.js +324 -0
  41. package/vendor/acp/claude-agent-acp/dist/session-mode.js +234 -0
  42. package/vendor/acp/claude-agent-acp/dist/session-titles.js +199 -0
  43. package/vendor/acp/claude-agent-acp/dist/settings.js +185 -0
  44. package/vendor/acp/claude-agent-acp/dist/tool-result-meta.js +19 -0
  45. package/vendor/acp/claude-agent-acp/dist/tools.js +1235 -0
  46. package/vendor/acp/claude-agent-acp/dist/utils.js +81 -0
  47. package/vendor/acp/claude-agent-acp/package.json +7 -0
  48. package/vendor/acp/claude-agent-sdk/LICENSE.md +1 -0
  49. package/vendor/acp/claude-agent-sdk/agentSdkTypes.d.ts +1 -0
  50. package/vendor/acp/claude-agent-sdk/bridge.d.ts +378 -0
  51. package/vendor/acp/claude-agent-sdk/bridge.mjs +221 -0
  52. package/vendor/acp/claude-agent-sdk/browser-sdk.d.ts +107 -0
  53. package/vendor/acp/claude-agent-sdk/browser-sdk.js +185 -0
  54. package/vendor/acp/claude-agent-sdk/extractFromBunfs.d.ts +1 -0
  55. package/vendor/acp/claude-agent-sdk/extractFromBunfs.js +156 -0
  56. package/vendor/acp/claude-agent-sdk/manifest.json +65 -0
  57. package/vendor/acp/claude-agent-sdk/manifest.zst.json +73 -0
  58. package/vendor/acp/claude-agent-sdk/package.json +7 -0
  59. package/vendor/acp/claude-agent-sdk/sdk-tools.d.ts +4129 -0
  60. package/vendor/acp/claude-agent-sdk/sdk.d.ts +8687 -0
  61. package/vendor/acp/claude-agent-sdk/sdk.mjs +204 -0
  62. package/vendor/acp/codex-acp/LICENSE +190 -0
  63. package/vendor/acp/codex-acp/dist/index.js +34238 -0
  64. package/vendor/acp/codex-acp/package.json +7 -0
@@ -0,0 +1,199 @@
1
+ /**
2
+ * AI-generated session titles.
3
+ *
4
+ * Claude Code's own auto-title generation never runs under the Agent SDK — the
5
+ * latch that arms it is pre-set in the headless path — so `SDKSessionInfo.summary`
6
+ * degrades to the raw first prompt. This module asks the CLI for a real title
7
+ * instead, via the `generate_session_title` control request, and publishes it
8
+ * over ACP as a `session_info_update`.
9
+ *
10
+ * {@link SessionTitles} holds the per-session state and is owned by `Session`.
11
+ * `acp-agent.ts` drives it from four places: {@link SessionTitles.onPrompt} and
12
+ * {@link SessionTitles.onAssistantText} to collect text,
13
+ * {@link SessionTitles.onTurnEnd} at `session_state_changed: idle`, and
14
+ * {@link SessionTitles.reset} on `conversation_reset`.
15
+ */
16
+ import { getSessionInfo } from "../../claude-agent-sdk/sdk.mjs";
17
+ const MAX_TITLE_LENGTH = 256;
18
+ /** How much session text the title is generated from. The CLI caps its own
19
+ * title input at the same 1000 trailing characters. */
20
+ const MAX_TITLE_CONTEXT_LENGTH = 1000;
21
+ /** Below this the generator returns null without calling the model, so there is
22
+ * nothing to gain by asking yet. */
23
+ const MIN_TITLE_CONTEXT_LENGTH = 10;
24
+ export function sanitizeTitle(text) {
25
+ // Replace newlines and collapse whitespace
26
+ const sanitized = text
27
+ .replace(/[\r\n]+/g, " ")
28
+ .replace(/\s+/g, " ")
29
+ .trim();
30
+ if (sanitized.length <= MAX_TITLE_LENGTH) {
31
+ return sanitized;
32
+ }
33
+ return sanitized.slice(0, MAX_TITLE_LENGTH - 1) + "…";
34
+ }
35
+ /** Rolling text a title is generated from: `previous` plus `text`, keeping only
36
+ * the trailing {@link MAX_TITLE_CONTEXT_LENGTH} characters. */
37
+ export function appendTitleContext(previous, text) {
38
+ const next = (previous ?? "") + text;
39
+ return next.length > MAX_TITLE_CONTEXT_LENGTH ? next.slice(-MAX_TITLE_CONTEXT_LENGTH) : next;
40
+ }
41
+ /** Whether this SDK still offers on-demand title generation. */
42
+ function supportsTitleGeneration(query) {
43
+ return typeof query.generateSessionTitle === "function";
44
+ }
45
+ /** One session's title: the text a generated title is derived from, whether a
46
+ * title has been settled on, and the last one published. Created with the
47
+ * session and reachable as `session.titles`. */
48
+ export class SessionTitles {
49
+ agent;
50
+ sessionId;
51
+ /** Last title pushed to the client via `session_info_update`, so an unchanged
52
+ * title is never re-notified. Undefined until the first push. */
53
+ lastTitle;
54
+ /** Rolling tail of this session's own user + assistant text, capped at
55
+ * {@link MAX_TITLE_CONTEXT_LENGTH}. Dropped once a title exists. */
56
+ context;
57
+ /** Set once this session has a title or has asked for one. A title is
58
+ * generated at most once per session: the SDK reports a generated title in
59
+ * the same field as a user `/rename`, so re-titling could silently overwrite
60
+ * one. Released by {@link reset}, and when generation yields nothing. */
61
+ settled = false;
62
+ constructor(agent, sessionId) {
63
+ this.agent = agent;
64
+ this.sessionId = sessionId;
65
+ }
66
+ /** Collect a prompt's own text for the title, skipping openers not worth
67
+ * titling after. No-op once the title is settled. */
68
+ onPrompt(prompt) {
69
+ if (this.settled) {
70
+ return;
71
+ }
72
+ const promptText = prompt
73
+ .flatMap((chunk) => (chunk.type === "text" ? [chunk.text] : []))
74
+ .join("\n");
75
+ this.context = appendTitleContext(this.context, `\n${promptText}\n`);
76
+ }
77
+ /** Collect the assistant's own answer for the title. Chunks are appended
78
+ * verbatim so streamed text reassembles. No-op once the title is settled. */
79
+ onAssistantText(content) {
80
+ if (this.settled || content.type !== "text") {
81
+ return;
82
+ }
83
+ this.context = appendTitleContext(this.context, content.text);
84
+ }
85
+ /** Drop the title state so the next turn re-evaluates. Used on
86
+ * `conversation_reset`, which mounts a fresh transcript. */
87
+ reset() {
88
+ this.settled = false;
89
+ this.context = undefined;
90
+ this.lastTitle = undefined;
91
+ }
92
+ /** Turn-end title handling. `idle` is the SDK's turn-over signal, so it is
93
+ * when a title may have landed or become generatable.
94
+ *
95
+ * `info.customTitle` is non-empty only once the session file holds a real
96
+ * title — a user `/rename` or one generated earlier; the SDK folds both into
97
+ * that one field. Adopt it and latch, so neither is ever titled over.
98
+ *
99
+ * With no title yet, ask the SDK to generate one in the background: it is a
100
+ * ~2s small-model call and turn-end must not wait on it. Only when that is
101
+ * not possible do we fall back to `summary`, which for an SDK-driven session
102
+ * is just the raw first prompt. */
103
+ async onTurnEnd(session) {
104
+ const info = await this.readSessionInfo(session);
105
+ if (info?.customTitle) {
106
+ this.settled = true;
107
+ this.context = undefined;
108
+ await this.publish(info.customTitle, info.lastModified);
109
+ return;
110
+ }
111
+ const fallback = info?.summary
112
+ ? { title: info.summary, lastModified: info.lastModified }
113
+ : undefined;
114
+ if (this.canRequest(session)) {
115
+ this.settled = true;
116
+ void this.requestGenerateTitle(session, fallback).catch((error) => {
117
+ this.agent.logger.error(`Session ${this.sessionId}: session title update failed: ${error}`);
118
+ });
119
+ return;
120
+ }
121
+ // Only while this session has no title of its own: `summary` degrades to the
122
+ // raw first prompt, which must never overwrite a generated title if the
123
+ // persisted one is slow to show up in `info`.
124
+ if (fallback && !this.settled) {
125
+ await this.publish(fallback.title, fallback.lastModified);
126
+ }
127
+ }
128
+ /** Read the SDK's stored info for this session. A missing session file or read
129
+ * error is non-fatal: the title is best-effort and another turn will retry. */
130
+ async readSessionInfo(session) {
131
+ try {
132
+ return await getSessionInfo(this.sessionId, { dir: session.cwd });
133
+ }
134
+ catch (error) {
135
+ this.agent.logger.error(`Session ${this.sessionId}: failed to read session info: ${error}`);
136
+ return undefined;
137
+ }
138
+ }
139
+ /** Notify the client of a title, unless it is the one we last sent. */
140
+ async publish(rawTitle, lastModified) {
141
+ const title = sanitizeTitle(rawTitle);
142
+ if (title === this.lastTitle) {
143
+ return;
144
+ }
145
+ this.lastTitle = title;
146
+ await this.agent.client.sessionUpdate({
147
+ sessionId: this.sessionId,
148
+ update: {
149
+ sessionUpdate: "session_info_update",
150
+ title,
151
+ updatedAt: new Date(lastModified).toISOString(),
152
+ },
153
+ });
154
+ }
155
+ /** Whether to ask the SDK for a generated title now: once per session, on a
156
+ * live query, with enough collected text for the generator to work with. */
157
+ canRequest(session) {
158
+ if (this.settled || session.queryClosed || session.cancelled) {
159
+ return false;
160
+ }
161
+ if (!supportsTitleGeneration(session.query)) {
162
+ return false;
163
+ }
164
+ return (this.context?.trim().length ?? 0) >= MIN_TITLE_CONTEXT_LENGTH;
165
+ }
166
+ /** Generate a title from the session's own text and publish it. `persist`
167
+ * writes it to the session file, which is what carries it into
168
+ * `session/list` and `session/load`. A null or failed generation releases the
169
+ * latch so a later turn retries, and publishes `fallback` (the stored
170
+ * summary) so a backend that can't generate titles is no worse off than
171
+ * before. */
172
+ async requestGenerateTitle(session, fallback) {
173
+ const description = this.context?.trim() ?? "";
174
+ let title = null;
175
+ try {
176
+ title =
177
+ (await session.query.generateSessionTitle?.(description, {
178
+ persist: true,
179
+ })) ?? null;
180
+ }
181
+ catch (error) {
182
+ this.agent.logger.error(`Session ${this.sessionId}: failed to generate a session title: ${error}`);
183
+ }
184
+ // A session torn down or replaced while the title was in flight must not
185
+ // adopt it.
186
+ if (this.agent.sessions[this.sessionId] !== session) {
187
+ return;
188
+ }
189
+ if (!title) {
190
+ this.settled = false;
191
+ if (fallback) {
192
+ await this.publish(fallback.title, fallback.lastModified);
193
+ }
194
+ return;
195
+ }
196
+ this.context = undefined;
197
+ await this.publish(title, Date.now());
198
+ }
199
+ }
@@ -0,0 +1,185 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ // resolveSettings and filterEscalatingDefaultMode are marked @alpha in the
4
+ // SDK; API may shift in a future release.
5
+ import { filterEscalatingDefaultMode, resolveSettings, } from "../../claude-agent-sdk/sdk.mjs";
6
+ import { CLAUDE_CONFIG_DIR } from "./acp-agent.js";
7
+ /**
8
+ * Permission rule format examples:
9
+ * - "Read" - matches all Read tool calls
10
+ * - "Read(./.env)" - matches specific path
11
+ * - "Read(./.env.*)" - glob pattern
12
+ * - "Read(./secrets/**)" - recursive glob
13
+ * - "Bash(npm run lint)" - exact command prefix
14
+ * - "Bash(npm run:*)" - command prefix with wildcard
15
+ *
16
+ * Docs: https://code.claude.com/docs/en/iam#tool-specific-permission-rules
17
+ */
18
+ function getManagedSettingsPath() {
19
+ switch (process.platform) {
20
+ case "darwin":
21
+ return "/Library/Application Support/ClaudeCode/managed-settings.json";
22
+ case "win32":
23
+ return "C:\\Program Files\\ClaudeCode\\managed-settings.json";
24
+ default:
25
+ return "/etc/claude-code/managed-settings.json";
26
+ }
27
+ }
28
+ /**
29
+ * Manages Claude Code settings using the SDK's `resolveSettings` merge engine
30
+ * so the values we see match what `query()` would observe.
31
+ *
32
+ * Watches the user/project/local/managed settings files for changes and
33
+ * re-resolves through the SDK on update. Escalating `permissions.defaultMode`
34
+ * values from repo-committed sources are filtered out via
35
+ * `filterEscalatingDefaultMode`, matching the CLI's trust policy.
36
+ */
37
+ export class SettingsManager {
38
+ cwd;
39
+ effective = {};
40
+ watchers = [];
41
+ onChange;
42
+ logger;
43
+ initialized = false;
44
+ disposed = false;
45
+ debounceTimer = null;
46
+ initPromise = null;
47
+ constructor(cwd, options) {
48
+ this.cwd = cwd;
49
+ this.onChange = options?.onChange;
50
+ this.logger = options?.logger ?? console;
51
+ }
52
+ /**
53
+ * Initialize the settings manager by loading all settings and setting up file watchers
54
+ */
55
+ async initialize() {
56
+ if (this.initialized) {
57
+ return;
58
+ }
59
+ if (this.initPromise) {
60
+ return this.initPromise;
61
+ }
62
+ this.disposed = false;
63
+ this.initPromise = this.loadAllSettings().then(() => {
64
+ if (!this.disposed) {
65
+ this.setupWatchers();
66
+ this.initialized = true;
67
+ }
68
+ this.initPromise = null;
69
+ });
70
+ return this.initPromise;
71
+ }
72
+ /**
73
+ * Paths the SDK reads when resolving settings for this cwd. Watching the
74
+ * containing directories means we pick up file creation as well as edits.
75
+ */
76
+ getWatchedPaths() {
77
+ return [
78
+ path.join(CLAUDE_CONFIG_DIR, "settings.json"),
79
+ path.join(this.cwd, ".claude", "settings.json"),
80
+ path.join(this.cwd, ".claude", "settings.local.json"),
81
+ getManagedSettingsPath(),
82
+ ];
83
+ }
84
+ /**
85
+ * Resolves the effective settings via the SDK and applies the CLI's trust
86
+ * filter for escalating `permissions.defaultMode` values.
87
+ */
88
+ async loadAllSettings() {
89
+ try {
90
+ const resolved = await resolveSettings({ cwd: this.cwd });
91
+ this.effective = filterEscalatingDefaultMode(resolved);
92
+ }
93
+ catch (error) {
94
+ this.logger.error("Failed to resolve settings:", error);
95
+ this.effective = {};
96
+ }
97
+ }
98
+ /**
99
+ * Sets up file watchers for all settings files
100
+ */
101
+ setupWatchers() {
102
+ for (const filePath of this.getWatchedPaths()) {
103
+ try {
104
+ const dir = path.dirname(filePath);
105
+ const filename = path.basename(filePath);
106
+ if (fs.existsSync(dir)) {
107
+ const watcher = fs.watch(dir, (eventType, changedFilename) => {
108
+ if (changedFilename === filename) {
109
+ this.handleSettingsChange();
110
+ }
111
+ });
112
+ watcher.on("error", (error) => {
113
+ this.logger.error(`Settings watcher error for ${filePath}:`, error);
114
+ });
115
+ this.watchers.push(watcher);
116
+ }
117
+ }
118
+ catch (error) {
119
+ this.logger.error(`Failed to set up watcher for ${filePath}:`, error);
120
+ }
121
+ }
122
+ }
123
+ /**
124
+ * Handles settings file changes with debouncing to avoid rapid reloads
125
+ */
126
+ handleSettingsChange() {
127
+ if (this.debounceTimer) {
128
+ clearTimeout(this.debounceTimer);
129
+ }
130
+ this.debounceTimer = setTimeout(async () => {
131
+ this.debounceTimer = null;
132
+ if (this.disposed) {
133
+ return;
134
+ }
135
+ try {
136
+ await this.loadAllSettings();
137
+ if (!this.disposed) {
138
+ this.onChange?.();
139
+ }
140
+ }
141
+ catch (error) {
142
+ this.logger.error("Failed to reload settings:", error);
143
+ }
144
+ }, 100);
145
+ }
146
+ /**
147
+ * Returns the current merged settings
148
+ */
149
+ getSettings() {
150
+ return this.effective;
151
+ }
152
+ /**
153
+ * Returns the current working directory
154
+ */
155
+ getCwd() {
156
+ return this.cwd;
157
+ }
158
+ /**
159
+ * Updates the working directory and reloads project-specific settings
160
+ */
161
+ async setCwd(cwd) {
162
+ if (this.cwd === cwd) {
163
+ return;
164
+ }
165
+ this.dispose();
166
+ this.cwd = cwd;
167
+ await this.initialize();
168
+ }
169
+ /**
170
+ * Disposes of file watchers and cleans up resources
171
+ */
172
+ dispose() {
173
+ this.disposed = true;
174
+ this.initialized = false;
175
+ this.initPromise = null;
176
+ if (this.debounceTimer) {
177
+ clearTimeout(this.debounceTimer);
178
+ this.debounceTimer = null;
179
+ }
180
+ for (const watcher of this.watchers) {
181
+ watcher.close();
182
+ }
183
+ this.watchers = [];
184
+ }
185
+ }
@@ -0,0 +1,19 @@
1
+ /** Validate the SDK's currently untyped tool_result_meta sidecar. Unknown
2
+ * non-execution kinds are preserved so newer CLIs remain forward-compatible. */
3
+ export function parseToolResultMeta(raw) {
4
+ if (!Array.isArray(raw))
5
+ return undefined;
6
+ let byToolUseId;
7
+ for (const entry of raw) {
8
+ if (typeof entry !== "object" || entry === null)
9
+ continue;
10
+ const { id, non_execution_kind, user_feedback } = entry;
11
+ if (typeof id !== "string" || typeof non_execution_kind !== "string")
12
+ continue;
13
+ (byToolUseId ??= new Map()).set(id, {
14
+ nonExecutionKind: non_execution_kind,
15
+ ...(typeof user_feedback === "string" ? { userFeedback: user_feedback } : {}),
16
+ });
17
+ }
18
+ return byToolUseId;
19
+ }