iosm-cli 0.2.13 → 0.2.15
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/.npmignore +2 -0
- package/CHANGELOG.md +49 -0
- package/README.md +12 -2
- package/dist/cli/args.d.ts +1 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +9 -2
- package/dist/cli/args.js.map +1 -1
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +17 -2
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/command-dispatcher.d.ts +16 -0
- package/dist/core/command-dispatcher.d.ts.map +1 -0
- package/dist/core/command-dispatcher.js +678 -0
- package/dist/core/command-dispatcher.js.map +1 -0
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +13 -1
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-resolver.d.ts +2 -2
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +1 -2
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/provider-policy.d.ts +7 -0
- package/dist/core/provider-policy.d.ts.map +1 -0
- package/dist/core/provider-policy.js +19 -0
- package/dist/core/provider-policy.js.map +1 -0
- package/dist/core/settings-manager.d.ts +25 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +32 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +4 -1
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/subagent-background-runs.d.ts +56 -0
- package/dist/core/subagent-background-runs.d.ts.map +1 -0
- package/dist/core/subagent-background-runs.js +275 -0
- package/dist/core/subagent-background-runs.js.map +1 -0
- package/dist/core/tools/task.d.ts.map +1 -1
- package/dist/core/tools/task.js +39 -35
- package/dist/core/tools/task.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +16 -2
- package/dist/main.js.map +1 -1
- package/dist/modes/index.d.ts +1 -0
- package/dist/modes/index.d.ts.map +1 -1
- package/dist/modes/index.js +1 -0
- package/dist/modes/index.js.map +1 -1
- package/dist/modes/interactive/components/login-dialog.d.ts +1 -1
- package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
- package/dist/modes/interactive/components/login-dialog.js +1 -4
- package/dist/modes/interactive/components/login-dialog.js.map +1 -1
- package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/oauth-selector.js +1 -2
- package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +7 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +253 -10
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +11 -1
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +54 -0
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +87 -3
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +69 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/modes/telegram/telegram-bridge-mode.d.ts +15 -0
- package/dist/modes/telegram/telegram-bridge-mode.d.ts.map +1 -0
- package/dist/modes/telegram/telegram-bridge-mode.js +2164 -0
- package/dist/modes/telegram/telegram-bridge-mode.js.map +1 -0
- package/docs/cli-reference.md +10 -1
- package/docs/configuration.md +21 -0
- package/docs/rpc-json-sdk.md +23 -0
- package/examples/extensions/README.md +1 -2
- package/package.json +4 -3
- package/examples/extensions/antigravity-image-gen.ts +0 -415
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { getShareViewerUrl } from "../config.js";
|
|
6
|
+
import { getChangelogPath, parseChangelog } from "../utils/changelog.js";
|
|
7
|
+
const CHECKPOINT_LABEL_PREFIX = "checkpoint:";
|
|
8
|
+
export function parseSlashArgs(input) {
|
|
9
|
+
const args = [];
|
|
10
|
+
let current = "";
|
|
11
|
+
let quote;
|
|
12
|
+
let escape = false;
|
|
13
|
+
for (const ch of input.trim()) {
|
|
14
|
+
if (escape) {
|
|
15
|
+
current += ch;
|
|
16
|
+
escape = false;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (ch === "\\") {
|
|
20
|
+
escape = true;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (quote) {
|
|
24
|
+
if (ch === quote) {
|
|
25
|
+
quote = undefined;
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
current += ch;
|
|
29
|
+
}
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (ch === '"' || ch === "'") {
|
|
33
|
+
quote = ch;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (/\s/.test(ch)) {
|
|
37
|
+
if (current) {
|
|
38
|
+
args.push(current);
|
|
39
|
+
current = "";
|
|
40
|
+
}
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
current += ch;
|
|
44
|
+
}
|
|
45
|
+
if (escape) {
|
|
46
|
+
current += "\\";
|
|
47
|
+
}
|
|
48
|
+
if (current) {
|
|
49
|
+
args.push(current);
|
|
50
|
+
}
|
|
51
|
+
return args;
|
|
52
|
+
}
|
|
53
|
+
function formatPermissionStatus(settingsManager) {
|
|
54
|
+
const mode = settingsManager.getPermissionMode();
|
|
55
|
+
const allowRules = settingsManager.getPermissionAllowRules();
|
|
56
|
+
const denyRules = settingsManager.getPermissionDenyRules();
|
|
57
|
+
return `Permissions: ${mode}${allowRules.length > 0 ? ` · allow rules: ${allowRules.length}` : ""}${denyRules.length > 0 ? ` · deny rules: ${denyRules.length}` : ""}`;
|
|
58
|
+
}
|
|
59
|
+
function buildSessionStatsText(session) {
|
|
60
|
+
const stats = session.getSessionStats();
|
|
61
|
+
let info = "Session Info\n\n";
|
|
62
|
+
info += `File: ${stats.sessionFile ?? "(ephemeral)"}\n`;
|
|
63
|
+
info += `ID: ${stats.sessionId}\n\n`;
|
|
64
|
+
info += "Messages\n";
|
|
65
|
+
info += `User: ${stats.userMessages}\n`;
|
|
66
|
+
info += `Assistant: ${stats.assistantMessages}\n`;
|
|
67
|
+
info += `Tool Calls: ${stats.toolCalls}\n`;
|
|
68
|
+
info += `Tool Results: ${stats.toolResults}\n`;
|
|
69
|
+
info += `Total: ${stats.totalMessages}\n\n`;
|
|
70
|
+
info += "Tokens\n";
|
|
71
|
+
info += `Input: ${stats.tokens.input.toLocaleString()}\n`;
|
|
72
|
+
info += `Output: ${stats.tokens.output.toLocaleString()}\n`;
|
|
73
|
+
if (stats.tokens.cacheRead > 0) {
|
|
74
|
+
info += `Cache Read: ${stats.tokens.cacheRead.toLocaleString()}\n`;
|
|
75
|
+
}
|
|
76
|
+
if (stats.tokens.cacheWrite > 0) {
|
|
77
|
+
info += `Cache Write: ${stats.tokens.cacheWrite.toLocaleString()}\n`;
|
|
78
|
+
}
|
|
79
|
+
info += `Total: ${stats.tokens.total.toLocaleString()}\n`;
|
|
80
|
+
if (stats.cost > 0) {
|
|
81
|
+
info += `\nCost\nTotal: ${stats.cost.toFixed(4)}\n`;
|
|
82
|
+
}
|
|
83
|
+
return info.trimEnd();
|
|
84
|
+
}
|
|
85
|
+
function normalizePermissionRule(rule) {
|
|
86
|
+
const normalized = rule.trim();
|
|
87
|
+
if (!normalized || !normalized.includes(":"))
|
|
88
|
+
return undefined;
|
|
89
|
+
return normalized;
|
|
90
|
+
}
|
|
91
|
+
function extractEntryText(entry) {
|
|
92
|
+
if (entry.type === "message") {
|
|
93
|
+
const message = entry.message;
|
|
94
|
+
if (typeof message.command === "string" && message.command.trim().length > 0) {
|
|
95
|
+
return message.command.replace(/\s+/g, " ").trim();
|
|
96
|
+
}
|
|
97
|
+
if (!("content" in message)) {
|
|
98
|
+
return "";
|
|
99
|
+
}
|
|
100
|
+
const content = message.content;
|
|
101
|
+
if (typeof content === "string")
|
|
102
|
+
return content.replace(/\s+/g, " ").trim();
|
|
103
|
+
if (Array.isArray(content)) {
|
|
104
|
+
return content
|
|
105
|
+
.filter((part) => part.type === "text")
|
|
106
|
+
.map((part) => part.text)
|
|
107
|
+
.join(" ")
|
|
108
|
+
.replace(/\s+/g, " ")
|
|
109
|
+
.trim();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (entry.type === "branch_summary" || entry.type === "compaction") {
|
|
113
|
+
return entry.summary.replace(/\s+/g, " ").trim();
|
|
114
|
+
}
|
|
115
|
+
if (entry.type === "label") {
|
|
116
|
+
return entry.label?.trim() ?? "";
|
|
117
|
+
}
|
|
118
|
+
if (entry.type === "session_info") {
|
|
119
|
+
return entry.name?.trim() ?? "";
|
|
120
|
+
}
|
|
121
|
+
if (entry.type === "model_change") {
|
|
122
|
+
return `${entry.provider}/${entry.modelId}`;
|
|
123
|
+
}
|
|
124
|
+
if (entry.type === "thinking_level_change") {
|
|
125
|
+
return entry.thinkingLevel;
|
|
126
|
+
}
|
|
127
|
+
return "";
|
|
128
|
+
}
|
|
129
|
+
function summarizeEntry(entry) {
|
|
130
|
+
const text = extractEntryText(entry);
|
|
131
|
+
if (!text)
|
|
132
|
+
return "";
|
|
133
|
+
const max = 72;
|
|
134
|
+
return text.length > max ? `${text.slice(0, max - 3)}...` : text;
|
|
135
|
+
}
|
|
136
|
+
function flattenTree(nodes, depth = 0, lines = [], currentLeafId = null) {
|
|
137
|
+
for (const node of nodes) {
|
|
138
|
+
const marker = node.entry.id === currentLeafId ? "*" : " ";
|
|
139
|
+
const indent = " ".repeat(depth);
|
|
140
|
+
const label = node.label ? ` [${node.label}]` : "";
|
|
141
|
+
const summary = summarizeEntry(node.entry);
|
|
142
|
+
lines.push(`${marker} ${indent}${node.entry.id} (${node.entry.type}${label})${summary ? ` - ${summary}` : ""}`);
|
|
143
|
+
flattenTree(node.children, depth + 1, lines, currentLeafId);
|
|
144
|
+
}
|
|
145
|
+
return lines;
|
|
146
|
+
}
|
|
147
|
+
function buildTreeText(session) {
|
|
148
|
+
const tree = session.sessionManager.getTree();
|
|
149
|
+
if (tree.length === 0) {
|
|
150
|
+
return "No entries in session.";
|
|
151
|
+
}
|
|
152
|
+
const currentLeafId = session.sessionManager.getLeafId();
|
|
153
|
+
const lines = flattenTree(tree, 0, [], currentLeafId);
|
|
154
|
+
lines.push("");
|
|
155
|
+
lines.push(currentLeafId ? `Current leaf: ${currentLeafId}` : "Current leaf: (root)");
|
|
156
|
+
lines.push("Usage: /tree [list|<entry-id>|goto <entry-id>]");
|
|
157
|
+
return lines.join("\n");
|
|
158
|
+
}
|
|
159
|
+
function parseCheckpointNameFromLabel(label) {
|
|
160
|
+
if (!label)
|
|
161
|
+
return undefined;
|
|
162
|
+
if (!label.startsWith(CHECKPOINT_LABEL_PREFIX))
|
|
163
|
+
return undefined;
|
|
164
|
+
const name = label.slice(CHECKPOINT_LABEL_PREFIX.length).trim();
|
|
165
|
+
return name.length > 0 ? name : undefined;
|
|
166
|
+
}
|
|
167
|
+
function buildCheckpointLabel(name) {
|
|
168
|
+
return `${CHECKPOINT_LABEL_PREFIX}${name}`;
|
|
169
|
+
}
|
|
170
|
+
function normalizeCheckpointName(raw) {
|
|
171
|
+
const normalized = raw.replace(/\s+/g, " ").trim();
|
|
172
|
+
if (!normalized)
|
|
173
|
+
return undefined;
|
|
174
|
+
if (normalized.length > 80)
|
|
175
|
+
return undefined;
|
|
176
|
+
return normalized;
|
|
177
|
+
}
|
|
178
|
+
function getSessionCheckpoints(session) {
|
|
179
|
+
const active = new Map();
|
|
180
|
+
for (const entry of session.sessionManager.getEntries()) {
|
|
181
|
+
if (entry.type !== "label")
|
|
182
|
+
continue;
|
|
183
|
+
const name = parseCheckpointNameFromLabel(entry.label);
|
|
184
|
+
if (!name) {
|
|
185
|
+
active.delete(entry.targetId);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
active.set(entry.targetId, {
|
|
189
|
+
name,
|
|
190
|
+
targetId: entry.targetId,
|
|
191
|
+
labelEntryId: entry.id,
|
|
192
|
+
timestamp: entry.timestamp,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return [...active.values()].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
196
|
+
}
|
|
197
|
+
function buildDefaultCheckpointName(checkpoints) {
|
|
198
|
+
const used = new Set(checkpoints.map((checkpoint) => checkpoint.name.toLowerCase()));
|
|
199
|
+
let index = 1;
|
|
200
|
+
while (used.has(`cp-${index}`)) {
|
|
201
|
+
index += 1;
|
|
202
|
+
}
|
|
203
|
+
return `cp-${index}`;
|
|
204
|
+
}
|
|
205
|
+
function formatCheckpointList(session, checkpoints) {
|
|
206
|
+
if (checkpoints.length === 0) {
|
|
207
|
+
return "No checkpoints yet.\nCreate one with: /checkpoint [name]";
|
|
208
|
+
}
|
|
209
|
+
const newestFirst = [...checkpoints].reverse();
|
|
210
|
+
const lines = newestFirst.map((checkpoint, index) => {
|
|
211
|
+
const target = session.sessionManager.getEntry(checkpoint.targetId);
|
|
212
|
+
const type = target?.type ?? "missing";
|
|
213
|
+
return `${index + 1}. ${checkpoint.name} -> ${checkpoint.targetId} (${type}) @ ${checkpoint.timestamp}`;
|
|
214
|
+
});
|
|
215
|
+
lines.push("");
|
|
216
|
+
lines.push("Usage: /rollback [name|index]");
|
|
217
|
+
return lines.join("\n");
|
|
218
|
+
}
|
|
219
|
+
async function runCommandCapture(command, args) {
|
|
220
|
+
return await new Promise((resolve, reject) => {
|
|
221
|
+
const child = spawn(command, args);
|
|
222
|
+
let stdout = "";
|
|
223
|
+
let stderr = "";
|
|
224
|
+
child.stdout?.on("data", (chunk) => {
|
|
225
|
+
stdout += chunk.toString();
|
|
226
|
+
});
|
|
227
|
+
child.stderr?.on("data", (chunk) => {
|
|
228
|
+
stderr += chunk.toString();
|
|
229
|
+
});
|
|
230
|
+
child.on("error", reject);
|
|
231
|
+
child.on("close", (code) => {
|
|
232
|
+
resolve({ code, stdout, stderr });
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
export async function dispatchBuiltinSlashCommand(text, context) {
|
|
237
|
+
if (!text.startsWith("/")) {
|
|
238
|
+
return { handled: false };
|
|
239
|
+
}
|
|
240
|
+
const args = parseSlashArgs(text);
|
|
241
|
+
const commandToken = args[0]?.toLowerCase();
|
|
242
|
+
if (!commandToken?.startsWith("/")) {
|
|
243
|
+
return { handled: false };
|
|
244
|
+
}
|
|
245
|
+
const command = commandToken.slice(1);
|
|
246
|
+
const rest = args.slice(1);
|
|
247
|
+
const { session, settingsManager } = context;
|
|
248
|
+
if (command === "help") {
|
|
249
|
+
return {
|
|
250
|
+
handled: true,
|
|
251
|
+
level: "status",
|
|
252
|
+
text: [
|
|
253
|
+
"Core commands:",
|
|
254
|
+
"/status /abort /yolo /permissions /model /new /resume /session /name /copy /export /fork",
|
|
255
|
+
"/compact /reload /tree /checkpoint /rollback /changelog /share /logout",
|
|
256
|
+
].join("\n"),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
if (command === "yolo") {
|
|
260
|
+
const value = rest[0]?.toLowerCase();
|
|
261
|
+
if (!value) {
|
|
262
|
+
const nextMode = settingsManager.getPermissionMode() === "yolo" ? "ask" : "yolo";
|
|
263
|
+
settingsManager.setPermissionMode(nextMode);
|
|
264
|
+
return { handled: true, level: "status", message: `YOLO mode: ${nextMode === "yolo" ? "ON" : "OFF"}` };
|
|
265
|
+
}
|
|
266
|
+
if (value === "status") {
|
|
267
|
+
return {
|
|
268
|
+
handled: true,
|
|
269
|
+
level: "status",
|
|
270
|
+
message: `YOLO mode: ${settingsManager.getPermissionMode() === "yolo" ? "ON" : "OFF"}`,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
if (value === "on") {
|
|
274
|
+
settingsManager.setPermissionMode("yolo");
|
|
275
|
+
return { handled: true, level: "status", message: "YOLO mode: ON (tool confirmations disabled)" };
|
|
276
|
+
}
|
|
277
|
+
if (value === "off") {
|
|
278
|
+
settingsManager.setPermissionMode("ask");
|
|
279
|
+
return { handled: true, level: "status", message: "YOLO mode: OFF (tool confirmations enabled)" };
|
|
280
|
+
}
|
|
281
|
+
return { handled: true, level: "warning", message: "Usage: /yolo [on|off|status]" };
|
|
282
|
+
}
|
|
283
|
+
if (command === "permissions") {
|
|
284
|
+
const value = rest[0]?.toLowerCase();
|
|
285
|
+
if (!value || value === "status") {
|
|
286
|
+
return { handled: true, level: "status", message: formatPermissionStatus(settingsManager) };
|
|
287
|
+
}
|
|
288
|
+
if (value === "ask" || value === "auto" || value === "yolo") {
|
|
289
|
+
settingsManager.setPermissionMode(value);
|
|
290
|
+
return { handled: true, level: "status", message: `Permissions: ${value}` };
|
|
291
|
+
}
|
|
292
|
+
if (value === "allow" || value === "deny") {
|
|
293
|
+
const action = rest[1]?.toLowerCase();
|
|
294
|
+
const isAllow = value === "allow";
|
|
295
|
+
let rules = isAllow ? settingsManager.getPermissionAllowRules() : settingsManager.getPermissionDenyRules();
|
|
296
|
+
if (action === "list") {
|
|
297
|
+
if (rules.length === 0) {
|
|
298
|
+
return { handled: true, level: "status", message: `Permissions ${value} rules: (empty)` };
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
handled: true,
|
|
302
|
+
level: "status",
|
|
303
|
+
text: rules.map((rule) => `- ${rule}`).join("\n"),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
if (action === "add") {
|
|
307
|
+
const rawRule = rest.slice(2).join(" ");
|
|
308
|
+
const normalizedRule = normalizePermissionRule(rawRule);
|
|
309
|
+
if (!normalizedRule) {
|
|
310
|
+
return { handled: true, level: "warning", message: `Usage: /permissions ${value} add <tool:match>` };
|
|
311
|
+
}
|
|
312
|
+
if (!rules.includes(normalizedRule)) {
|
|
313
|
+
rules = [...rules, normalizedRule];
|
|
314
|
+
if (isAllow) {
|
|
315
|
+
settingsManager.setPermissionAllowRules(rules);
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
settingsManager.setPermissionDenyRules(rules);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return { handled: true, level: "status", message: `Added ${value} rule: ${normalizedRule}` };
|
|
322
|
+
}
|
|
323
|
+
if (action === "remove") {
|
|
324
|
+
const rawRule = rest.slice(2).join(" ").trim();
|
|
325
|
+
if (!rawRule) {
|
|
326
|
+
return {
|
|
327
|
+
handled: true,
|
|
328
|
+
level: "warning",
|
|
329
|
+
message: `Usage: /permissions ${value} remove <tool:match>`,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
rules = rules.filter((rule) => rule !== rawRule);
|
|
333
|
+
if (isAllow) {
|
|
334
|
+
settingsManager.setPermissionAllowRules(rules);
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
settingsManager.setPermissionDenyRules(rules);
|
|
338
|
+
}
|
|
339
|
+
return { handled: true, level: "status", message: `Removed ${value} rule: ${rawRule}` };
|
|
340
|
+
}
|
|
341
|
+
return { handled: true, level: "warning", message: `Usage: /permissions ${value} [list|add|remove]` };
|
|
342
|
+
}
|
|
343
|
+
return {
|
|
344
|
+
handled: true,
|
|
345
|
+
level: "warning",
|
|
346
|
+
message: "Usage: /permissions [ask|auto|yolo|status|allow|deny]",
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
if (command === "abort") {
|
|
350
|
+
await session.abort();
|
|
351
|
+
return { handled: true, level: "status", message: "Abort signal sent." };
|
|
352
|
+
}
|
|
353
|
+
if (command === "compact") {
|
|
354
|
+
const customInstructions = rest.join(" ").trim() || undefined;
|
|
355
|
+
try {
|
|
356
|
+
const result = await session.compact(customInstructions);
|
|
357
|
+
return {
|
|
358
|
+
handled: true,
|
|
359
|
+
level: "status",
|
|
360
|
+
message: `Compaction complete. First kept: ${result.firstKeptEntryId}. Tokens before: ${result.tokensBefore}.`,
|
|
361
|
+
text: result.summary,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
366
|
+
return {
|
|
367
|
+
handled: true,
|
|
368
|
+
level: "warning",
|
|
369
|
+
message: `Compaction failed: ${message}`,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (command === "status") {
|
|
374
|
+
const model = session.model ? `${session.model.provider}/${session.model.id}` : "not selected";
|
|
375
|
+
const sessionLabel = session.sessionName || session.sessionId || "(unknown)";
|
|
376
|
+
const lines = [
|
|
377
|
+
`Model: ${model}`,
|
|
378
|
+
`Session: ${sessionLabel}`,
|
|
379
|
+
`Streaming: ${session.isStreaming ? "yes" : "no"}`,
|
|
380
|
+
`Compacting: ${session.isCompacting ? "yes" : "no"}`,
|
|
381
|
+
`Queued messages: ${session.pendingMessageCount}`,
|
|
382
|
+
formatPermissionStatus(settingsManager),
|
|
383
|
+
];
|
|
384
|
+
return { handled: true, level: "status", text: lines.join("\n") };
|
|
385
|
+
}
|
|
386
|
+
if (command === "new" || command === "clear") {
|
|
387
|
+
const cancelled = !(await session.newSession());
|
|
388
|
+
return {
|
|
389
|
+
handled: true,
|
|
390
|
+
level: "status",
|
|
391
|
+
message: cancelled ? "New session cancelled." : "Started new session.",
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
if (command === "reload") {
|
|
395
|
+
if (session.isStreaming) {
|
|
396
|
+
return { handled: true, level: "warning", message: "Wait for the current response to finish before reloading." };
|
|
397
|
+
}
|
|
398
|
+
if (session.isCompacting) {
|
|
399
|
+
return { handled: true, level: "warning", message: "Wait for compaction to finish before reloading." };
|
|
400
|
+
}
|
|
401
|
+
await session.reload();
|
|
402
|
+
return { handled: true, level: "status", message: "Reloaded extensions, skills, prompts, and themes." };
|
|
403
|
+
}
|
|
404
|
+
if (command === "session") {
|
|
405
|
+
return { handled: true, level: "status", text: buildSessionStatsText(session) };
|
|
406
|
+
}
|
|
407
|
+
if (command === "name") {
|
|
408
|
+
const name = rest.join(" ").trim();
|
|
409
|
+
if (!name) {
|
|
410
|
+
return { handled: true, level: "warning", message: "Usage: /name <session-name>" };
|
|
411
|
+
}
|
|
412
|
+
session.setSessionName(name);
|
|
413
|
+
return { handled: true, level: "status", message: `Session name set: ${name}` };
|
|
414
|
+
}
|
|
415
|
+
if (command === "copy") {
|
|
416
|
+
const textResult = session.getLastAssistantText();
|
|
417
|
+
if (!textResult) {
|
|
418
|
+
return { handled: true, level: "warning", message: "No agent messages to copy yet." };
|
|
419
|
+
}
|
|
420
|
+
return { handled: true, level: "status", text: textResult };
|
|
421
|
+
}
|
|
422
|
+
if (command === "export") {
|
|
423
|
+
const outputPath = rest.length > 0 ? rest.join(" ") : undefined;
|
|
424
|
+
const path = await session.exportToHtml(outputPath);
|
|
425
|
+
return { handled: true, level: "status", message: `Session exported to: ${path}`, filePath: path };
|
|
426
|
+
}
|
|
427
|
+
if (command === "model") {
|
|
428
|
+
const value = rest.join(" ").trim();
|
|
429
|
+
if (!value) {
|
|
430
|
+
const current = session.model;
|
|
431
|
+
if (!current) {
|
|
432
|
+
return { handled: true, level: "warning", message: "No model selected. Usage: /model <provider/model-id>" };
|
|
433
|
+
}
|
|
434
|
+
return {
|
|
435
|
+
handled: true,
|
|
436
|
+
level: "status",
|
|
437
|
+
message: `Current model: ${current.provider}/${current.id}`,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
if (value.toLowerCase() === "cycle") {
|
|
441
|
+
const next = await session.cycleModel();
|
|
442
|
+
if (!next) {
|
|
443
|
+
return { handled: true, level: "warning", message: "No next model available to cycle." };
|
|
444
|
+
}
|
|
445
|
+
return {
|
|
446
|
+
handled: true,
|
|
447
|
+
level: "status",
|
|
448
|
+
message: `Model set: ${next.model.provider}/${next.model.id} (thinking: ${next.thinkingLevel})`,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
const allModels = await session.modelRegistry.getAvailable();
|
|
452
|
+
let selected = allModels.find((model) => `${model.provider}/${model.id}`.toLowerCase() === value.toLowerCase()) ??
|
|
453
|
+
allModels.find((model) => model.id.toLowerCase() === value.toLowerCase());
|
|
454
|
+
if (!selected) {
|
|
455
|
+
const asProviderModel = value.split("/");
|
|
456
|
+
if (asProviderModel.length === 2) {
|
|
457
|
+
const provider = asProviderModel[0]?.trim().toLowerCase();
|
|
458
|
+
const modelId = asProviderModel[1]?.trim().toLowerCase();
|
|
459
|
+
selected = allModels.find((model) => model.provider.toLowerCase() === provider && model.id.toLowerCase() === modelId);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (!selected) {
|
|
463
|
+
return {
|
|
464
|
+
handled: true,
|
|
465
|
+
level: "warning",
|
|
466
|
+
message: `Model not found: ${value}. Use /model <provider/model-id>.`,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
await session.setModel(selected);
|
|
470
|
+
return { handled: true, level: "status", message: `Model set: ${selected.provider}/${selected.id}` };
|
|
471
|
+
}
|
|
472
|
+
if (command === "tree") {
|
|
473
|
+
const action = rest[0]?.toLowerCase();
|
|
474
|
+
if (!action || action === "list" || action === "ls") {
|
|
475
|
+
return { handled: true, level: "status", text: buildTreeText(session) };
|
|
476
|
+
}
|
|
477
|
+
if (action === "help" || action === "-h" || action === "--help") {
|
|
478
|
+
return {
|
|
479
|
+
handled: true,
|
|
480
|
+
level: "status",
|
|
481
|
+
text: "Usage:\n /tree\n /tree <entry-id>\n /tree goto <entry-id>\n /tree list",
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
const targetId = action === "goto" ? rest.slice(1).join(" ").trim() : rest.join(" ").trim();
|
|
485
|
+
if (!targetId) {
|
|
486
|
+
return { handled: true, level: "warning", message: "Usage: /tree [list|<entry-id>|goto <entry-id>]" };
|
|
487
|
+
}
|
|
488
|
+
try {
|
|
489
|
+
const result = await session.navigateTree(targetId, { summarize: false });
|
|
490
|
+
if (result.aborted || result.cancelled) {
|
|
491
|
+
return { handled: true, level: "warning", message: "Tree navigation cancelled." };
|
|
492
|
+
}
|
|
493
|
+
return {
|
|
494
|
+
handled: true,
|
|
495
|
+
level: "status",
|
|
496
|
+
message: `Navigated to: ${targetId}`,
|
|
497
|
+
text: result.editorText,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
catch (error) {
|
|
501
|
+
return {
|
|
502
|
+
handled: true,
|
|
503
|
+
level: "error",
|
|
504
|
+
message: `Tree navigation failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
if (command === "checkpoint") {
|
|
509
|
+
const subcommand = rest[0]?.toLowerCase();
|
|
510
|
+
const checkpoints = getSessionCheckpoints(session);
|
|
511
|
+
if (subcommand === "list" || subcommand === "ls") {
|
|
512
|
+
return { handled: true, level: "status", text: formatCheckpointList(session, checkpoints) };
|
|
513
|
+
}
|
|
514
|
+
if (subcommand === "help" || subcommand === "-h" || subcommand === "--help") {
|
|
515
|
+
return {
|
|
516
|
+
handled: true,
|
|
517
|
+
level: "status",
|
|
518
|
+
text: "Usage:\n /checkpoint [name]\n /checkpoint list",
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
const leafId = session.sessionManager.getLeafId();
|
|
522
|
+
if (!leafId) {
|
|
523
|
+
return { handled: true, level: "warning", message: "Cannot create checkpoint yet (session has no entries)." };
|
|
524
|
+
}
|
|
525
|
+
const requestedName = rest.join(" ");
|
|
526
|
+
const name = requestedName ? normalizeCheckpointName(requestedName) : buildDefaultCheckpointName(checkpoints);
|
|
527
|
+
if (!name) {
|
|
528
|
+
return { handled: true, level: "warning", message: "Invalid checkpoint name. Use 1-80 visible characters." };
|
|
529
|
+
}
|
|
530
|
+
session.sessionManager.appendLabelChange(leafId, buildCheckpointLabel(name));
|
|
531
|
+
return { handled: true, level: "status", message: `Checkpoint saved: ${name} (${leafId})` };
|
|
532
|
+
}
|
|
533
|
+
if (command === "rollback") {
|
|
534
|
+
const subcommand = rest[0]?.toLowerCase();
|
|
535
|
+
const checkpoints = getSessionCheckpoints(session);
|
|
536
|
+
if (subcommand === "list" || subcommand === "ls") {
|
|
537
|
+
return { handled: true, level: "status", text: formatCheckpointList(session, checkpoints) };
|
|
538
|
+
}
|
|
539
|
+
if (subcommand === "help" || subcommand === "-h" || subcommand === "--help") {
|
|
540
|
+
return {
|
|
541
|
+
handled: true,
|
|
542
|
+
level: "status",
|
|
543
|
+
text: "Usage:\n /rollback\n /rollback <name>\n /rollback <index>\n /rollback list",
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
if (checkpoints.length === 0) {
|
|
547
|
+
return { handled: true, level: "warning", message: "No checkpoints available. Create one with /checkpoint." };
|
|
548
|
+
}
|
|
549
|
+
const newestFirst = [...checkpoints].reverse();
|
|
550
|
+
const selector = rest.join(" ").trim();
|
|
551
|
+
let target = newestFirst[0];
|
|
552
|
+
if (selector) {
|
|
553
|
+
const numeric = Number.parseInt(selector, 10);
|
|
554
|
+
if (Number.isFinite(numeric) && `${numeric}` === selector) {
|
|
555
|
+
target = newestFirst[numeric - 1];
|
|
556
|
+
if (!target) {
|
|
557
|
+
return { handled: true, level: "warning", message: `Checkpoint index ${numeric} is out of range.` };
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
else {
|
|
561
|
+
target = newestFirst.find((checkpoint) => checkpoint.name === selector);
|
|
562
|
+
if (!target) {
|
|
563
|
+
return { handled: true, level: "warning", message: `Checkpoint "${selector}" not found.` };
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (!target) {
|
|
568
|
+
return { handled: true, level: "warning", message: "No rollback target selected." };
|
|
569
|
+
}
|
|
570
|
+
try {
|
|
571
|
+
const result = await session.navigateTree(target.targetId, { summarize: false });
|
|
572
|
+
if (result.cancelled || result.aborted) {
|
|
573
|
+
return { handled: true, level: "warning", message: "Rollback cancelled." };
|
|
574
|
+
}
|
|
575
|
+
return {
|
|
576
|
+
handled: true,
|
|
577
|
+
level: "status",
|
|
578
|
+
message: `Rolled back to checkpoint: ${target.name}`,
|
|
579
|
+
text: result.editorText,
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
catch (error) {
|
|
583
|
+
return {
|
|
584
|
+
handled: true,
|
|
585
|
+
level: "error",
|
|
586
|
+
message: `Rollback failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
if (command === "changelog") {
|
|
591
|
+
const entries = parseChangelog(getChangelogPath());
|
|
592
|
+
const changelogText = entries.length > 0 ? entries.slice().reverse().map((entry) => entry.content).join("\n\n") : "No changelog entries found.";
|
|
593
|
+
return {
|
|
594
|
+
handled: true,
|
|
595
|
+
level: "status",
|
|
596
|
+
text: changelogText,
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
if (command === "share") {
|
|
600
|
+
const authResult = spawnSync("gh", ["auth", "status"], { encoding: "utf-8" });
|
|
601
|
+
if (authResult.status !== 0) {
|
|
602
|
+
return {
|
|
603
|
+
handled: true,
|
|
604
|
+
level: "warning",
|
|
605
|
+
message: "GitHub CLI is not logged in. Run `gh auth login` first.",
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
const tmpFile = join(tmpdir(), `iosm-session-${Date.now()}.html`);
|
|
609
|
+
try {
|
|
610
|
+
await session.exportToHtml(tmpFile);
|
|
611
|
+
const result = await runCommandCapture("gh", ["gist", "create", "--public=false", tmpFile]);
|
|
612
|
+
if (result.code !== 0) {
|
|
613
|
+
return {
|
|
614
|
+
handled: true,
|
|
615
|
+
level: "error",
|
|
616
|
+
message: `Failed to create gist: ${result.stderr.trim() || "Unknown error"}`,
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
const gistUrl = result.stdout.trim();
|
|
620
|
+
const gistId = gistUrl.split("/").pop();
|
|
621
|
+
if (!gistId) {
|
|
622
|
+
return { handled: true, level: "error", message: "Failed to parse gist ID from gh output." };
|
|
623
|
+
}
|
|
624
|
+
return {
|
|
625
|
+
handled: true,
|
|
626
|
+
level: "status",
|
|
627
|
+
text: `Share URL: ${getShareViewerUrl(gistId)}\nGist: ${gistUrl}`,
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
finally {
|
|
631
|
+
if (existsSync(tmpFile)) {
|
|
632
|
+
rmSync(tmpFile, { force: true });
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
if (command === "auth" || command === "login") {
|
|
637
|
+
return {
|
|
638
|
+
handled: true,
|
|
639
|
+
level: "warning",
|
|
640
|
+
message: "Interactive /login flow is not available in Telegram bridge yet. Use local CLI /login, then continue remotely.",
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
if (command === "logout") {
|
|
644
|
+
const provider = rest.join(" ").trim() || session.model?.provider;
|
|
645
|
+
if (!provider) {
|
|
646
|
+
return { handled: true, level: "warning", message: "Usage: /logout <provider> (or select a model first)" };
|
|
647
|
+
}
|
|
648
|
+
session.modelRegistry.authStorage.logout(provider);
|
|
649
|
+
return { handled: true, level: "status", message: `Logged out provider: ${provider}` };
|
|
650
|
+
}
|
|
651
|
+
if (command === "resume") {
|
|
652
|
+
const sessionPath = rest.join(" ").trim();
|
|
653
|
+
if (!sessionPath) {
|
|
654
|
+
return { handled: true, level: "warning", message: "Usage: /resume <session-path>" };
|
|
655
|
+
}
|
|
656
|
+
const cancelled = !(await session.switchSession(sessionPath));
|
|
657
|
+
return {
|
|
658
|
+
handled: true,
|
|
659
|
+
level: "status",
|
|
660
|
+
message: cancelled ? "Session switch cancelled." : `Switched session: ${sessionPath}`,
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
if (command === "fork") {
|
|
664
|
+
const entryId = rest.join(" ").trim();
|
|
665
|
+
if (!entryId) {
|
|
666
|
+
return { handled: true, level: "warning", message: "Usage: /fork <entry-id>" };
|
|
667
|
+
}
|
|
668
|
+
const result = await session.fork(entryId);
|
|
669
|
+
return {
|
|
670
|
+
handled: true,
|
|
671
|
+
level: result.cancelled ? "warning" : "status",
|
|
672
|
+
message: result.cancelled ? "Fork cancelled." : `Forked from message: ${entryId}`,
|
|
673
|
+
text: result.selectedText,
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
return { handled: false };
|
|
677
|
+
}
|
|
678
|
+
//# sourceMappingURL=command-dispatcher.js.map
|