@selesai/code 0.13.33 → 0.13.34
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/CHANGELOG.md +12 -0
- package/dist/extensions/capability-gateway/catalog.ts +4 -1
- package/dist/extensions/capability-gateway/index.test.ts +93 -4
- package/dist/extensions/capability-gateway/index.ts +294 -59
- package/dist/extensions/capability-gateway/integration.test.ts +35 -0
- package/dist/extensions/capability-gateway/routing.test.ts +154 -1
- package/dist/extensions/capability-gateway/routing.ts +227 -45
- package/dist/extensions/jev/decisions.test.ts +37 -0
- package/dist/extensions/jev/decisions.ts +148 -43
- package/dist/extensions/jev-ask-tool.test.ts +436 -0
- package/dist/extensions/jev-ask-tool.ts +587 -0
- package/dist/extensions/package.json +1 -0
- package/dist/extensions/pi-hermes-memory/README.md +11 -36
- package/dist/extensions/pi-hermes-memory/src/config.ts +36 -8
- package/dist/extensions/pi-hermes-memory/src/constants.ts +5 -5
- package/dist/extensions/pi-hermes-memory/src/handlers/auto-consolidate.ts +60 -46
- package/dist/extensions/pi-hermes-memory/tests/config.test.ts +26 -2
- package/dist/extensions/pi-hermes-memory/tests/handlers/auto-consolidate.test.ts +7 -1
- package/dist/extensions/pi-intercom/index.ts +5 -1
- package/dist/extensions/rtk.test.ts +21 -13
- package/dist/extensions/tps.test.ts +32 -1
- package/dist/extensions/tps.ts +3 -1
- package/docs/settings.md +46 -7
- package/package.json +3 -3
|
@@ -75,13 +75,42 @@ export const DEFAULT_CONFIG_PATH = path.join(
|
|
|
75
75
|
AGENT_ROOT,
|
|
76
76
|
"hermes-memory-config.json",
|
|
77
77
|
);
|
|
78
|
+
export const DEFAULT_SETTINGS_PATH = path.join(AGENT_ROOT, "settings.json");
|
|
79
|
+
export const HERMES_MEMORY_SETTINGS_KEY = "hermesMemory";
|
|
78
80
|
|
|
79
|
-
|
|
81
|
+
function readConfigObject(configPath: string | undefined): Record<string, unknown> | undefined {
|
|
82
|
+
if (!configPath) return undefined;
|
|
80
83
|
try {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
const parsed: unknown = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
85
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
|
|
86
|
+
? parsed as Record<string, unknown>
|
|
87
|
+
: undefined;
|
|
88
|
+
} catch {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
94
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Read extension settings from settings.json, falling back to the legacy file. */
|
|
98
|
+
export function loadConfig(
|
|
99
|
+
configPath = DEFAULT_CONFIG_PATH,
|
|
100
|
+
settingsPath?: string,
|
|
101
|
+
): MemoryConfig {
|
|
102
|
+
try {
|
|
103
|
+
const legacyConfig = readConfigObject(configPath);
|
|
104
|
+
const resolvedSettingsPath = settingsPath
|
|
105
|
+
?? (configPath === DEFAULT_CONFIG_PATH ? DEFAULT_SETTINGS_PATH : undefined);
|
|
106
|
+
const settings = readConfigObject(resolvedSettingsPath);
|
|
107
|
+
const extensionSettings = settings?.[HERMES_MEMORY_SETTINGS_KEY];
|
|
108
|
+
if (legacyConfig || isRecord(extensionSettings)) {
|
|
109
|
+
const parsed: Record<string, unknown> = {
|
|
110
|
+
...(legacyConfig ?? {}),
|
|
111
|
+
...(isRecord(extensionSettings) ? extensionSettings : {}),
|
|
112
|
+
};
|
|
113
|
+
// New settings.json values take precedence over the legacy file.
|
|
85
114
|
const config: MemoryConfig = { ...DEFAULT_CONFIG };
|
|
86
115
|
const isNonNegativeNumber = (value: unknown): value is number => (
|
|
87
116
|
typeof value === "number" && Number.isFinite(value) && value >= 0
|
|
@@ -156,8 +185,7 @@ export function loadConfig(configPath = DEFAULT_CONFIG_PATH): MemoryConfig {
|
|
|
156
185
|
if (normalizedProjectsMemoryDir) config.projectsMemoryDir = normalizedProjectsMemoryDir;
|
|
157
186
|
}
|
|
158
187
|
if (
|
|
159
|
-
|
|
160
|
-
parsed.sessionSearch !== null &&
|
|
188
|
+
isRecord(parsed.sessionSearch) &&
|
|
161
189
|
isSessionSearchVariant(parsed.sessionSearch.variant)
|
|
162
190
|
) {
|
|
163
191
|
config.sessionSearch = { variant: parsed.sessionSearch.variant };
|
|
@@ -204,7 +232,7 @@ export function loadConfig(configPath = DEFAULT_CONFIG_PATH): MemoryConfig {
|
|
|
204
232
|
return config;
|
|
205
233
|
}
|
|
206
234
|
} catch {
|
|
207
|
-
// Fall back to defaults on parse
|
|
235
|
+
// Fall back to defaults on unexpected parse or access issues.
|
|
208
236
|
}
|
|
209
237
|
return { ...DEFAULT_CONFIG };
|
|
210
238
|
}
|
|
@@ -29,12 +29,12 @@ export const DEFAULT_NUDGE_TOOL_CALLS = 15;
|
|
|
29
29
|
export const DEFAULT_REVIEW_RECENT_MESSAGES = 0;
|
|
30
30
|
export const DEFAULT_FLUSH_RECENT_MESSAGES = 0;
|
|
31
31
|
/**
|
|
32
|
-
* A consolidation run pays child-process boot plus a full LLM turn
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
32
|
+
* A consolidation run pays child-process boot plus a full LLM turn. Five
|
|
33
|
+
* minutes leaves room for slower models and larger memory stores. Configured
|
|
34
|
+
* values are honored verbatim, including lower ones; `loadConfig` warns when a
|
|
35
|
+
* value below this is set.
|
|
36
36
|
*/
|
|
37
|
-
export const DEFAULT_CONSOLIDATION_TIMEOUT_MS =
|
|
37
|
+
export const DEFAULT_CONSOLIDATION_TIMEOUT_MS = 300000;
|
|
38
38
|
/** Wall-clock grace after overflow before an automatic consolidation may run. */
|
|
39
39
|
export const DEFAULT_OVERFLOW_GRACE_MS = 180000;
|
|
40
40
|
export const DEFAULT_FAILURE_INJECTION_MAX_AGE_DAYS = 7;
|
|
@@ -337,64 +337,78 @@ export function registerConsolidateCommand(
|
|
|
337
337
|
});
|
|
338
338
|
}
|
|
339
339
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
} catch {
|
|
346
|
-
// Best-effort only. If the command context is already stale, continue
|
|
347
|
-
// with the consolidation work rather than failing before it starts.
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
for (const item of targets) {
|
|
351
|
-
const entries = entriesForTarget(item.store, item.target);
|
|
352
|
-
|
|
353
|
-
if (entries.length === 0) {
|
|
354
|
-
results.push(`${item.label}: (empty, nothing to consolidate)`);
|
|
355
|
-
continue;
|
|
340
|
+
const setStatus = (text?: string) => {
|
|
341
|
+
try {
|
|
342
|
+
ctx.ui.setStatus("pi-hermes-memory:consolidation", text);
|
|
343
|
+
} catch {
|
|
344
|
+
// Best-effort progress feedback only.
|
|
356
345
|
}
|
|
346
|
+
};
|
|
357
347
|
|
|
348
|
+
setStatus("Preparing memory consolidation…");
|
|
349
|
+
try {
|
|
358
350
|
try {
|
|
359
351
|
ctx.ui.notify(
|
|
360
|
-
|
|
352
|
+
`🔄 Starting memory consolidation for ${targets.length} target${targets.length === 1 ? "" : "s"}...`,
|
|
361
353
|
"info",
|
|
362
354
|
);
|
|
363
355
|
} catch {
|
|
364
|
-
// Best-effort
|
|
356
|
+
// Best-effort only. If the command context is already stale, continue
|
|
357
|
+
// with the consolidation work rather than failing before it starts.
|
|
365
358
|
}
|
|
366
359
|
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
360
|
+
for (const [index, item] of targets.entries()) {
|
|
361
|
+
const entries = entriesForTarget(item.store, item.target);
|
|
362
|
+
|
|
363
|
+
if (entries.length === 0) {
|
|
364
|
+
results.push(`${item.label}: (empty, nothing to consolidate)`);
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
setStatus(`Consolidating ${item.label} (${index + 1}/${targets.length})…`);
|
|
369
|
+
try {
|
|
370
|
+
ctx.ui.notify(
|
|
371
|
+
`⏳ Consolidating ${item.label}...`,
|
|
372
|
+
"info",
|
|
373
|
+
);
|
|
374
|
+
} catch {
|
|
375
|
+
// Best-effort progress feedback only.
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const result = await triggerConsolidation(
|
|
379
|
+
pi,
|
|
380
|
+
item.store,
|
|
381
|
+
item.target,
|
|
382
|
+
ctx.signal,
|
|
383
|
+
timeoutMs,
|
|
384
|
+
item.toolTarget,
|
|
385
|
+
llmConfig,
|
|
386
|
+
ctx,
|
|
387
|
+
dbManager,
|
|
388
|
+
activeProjectName,
|
|
389
|
+
deps,
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
if (result.consolidated) {
|
|
393
|
+
await item.store.loadFromDisk();
|
|
394
|
+
results.push(`${item.label}: ✅ consolidated`);
|
|
395
|
+
} else {
|
|
396
|
+
results.push(`${item.label}: ❌ ${result.error}`);
|
|
397
|
+
}
|
|
386
398
|
}
|
|
387
|
-
}
|
|
388
399
|
|
|
389
|
-
|
|
400
|
+
const summary = `\n 🔄 Memory Consolidation\n ${"─".repeat(30)}\n${results.map((r) => ` ${r}`).join("\n")}`;
|
|
390
401
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
402
|
+
try {
|
|
403
|
+
ctx.ui.notify(summary, "info");
|
|
404
|
+
} catch {
|
|
405
|
+
// Child consolidation can indirectly trigger a runtime reload/session
|
|
406
|
+
// replacement. If that happens, the original command ctx is stale by
|
|
407
|
+
// the time we reach the final summary, so the command should exit
|
|
408
|
+
// quietly instead of surfacing a stale-ctx error.
|
|
409
|
+
}
|
|
410
|
+
} finally {
|
|
411
|
+
setStatus(undefined);
|
|
398
412
|
}
|
|
399
413
|
},
|
|
400
414
|
});
|
|
@@ -7,9 +7,11 @@ import { loadConfig } from "../src/config.js";
|
|
|
7
7
|
import { AGENT_ROOT } from "../src/paths.js";
|
|
8
8
|
|
|
9
9
|
const TEST_CONFIG_PATH = path.join(os.tmpdir(), `hermes-memory-config-test-${process.pid}.json`);
|
|
10
|
+
const TEST_SETTINGS_PATH = path.join(os.tmpdir(), `hermes-settings-test-${process.pid}.json`);
|
|
10
11
|
|
|
11
12
|
afterEach(() => {
|
|
12
13
|
fs.rmSync(TEST_CONFIG_PATH, { force: true });
|
|
14
|
+
fs.rmSync(TEST_SETTINGS_PATH, { force: true });
|
|
13
15
|
});
|
|
14
16
|
|
|
15
17
|
describe("loadConfig", () => {
|
|
@@ -30,7 +32,7 @@ describe("loadConfig", () => {
|
|
|
30
32
|
assert.strictEqual(config.flushRecentMessages, 0);
|
|
31
33
|
assert.strictEqual(config.memoryOverflowStrategy, "auto-consolidate");
|
|
32
34
|
assert.strictEqual(config.autoConsolidate, true);
|
|
33
|
-
assert.strictEqual(config.consolidationTimeoutMs,
|
|
35
|
+
assert.strictEqual(config.consolidationTimeoutMs, 300000);
|
|
34
36
|
assert.strictEqual(config.overflowGraceMs, 180000);
|
|
35
37
|
assert.strictEqual(config.autoConsolidationWarnOnFailure, true);
|
|
36
38
|
assert.strictEqual(config.failureInjectionEnabled, true);
|
|
@@ -65,7 +67,7 @@ describe("loadConfig", () => {
|
|
|
65
67
|
"a lower configured value must be honored, not clamped",
|
|
66
68
|
);
|
|
67
69
|
assert.strictEqual(warnings.length, 1, "a sub-default value should warn once");
|
|
68
|
-
assert.match(warnings[0], /60000ms.*below the
|
|
70
|
+
assert.match(warnings[0], /60000ms.*below the 300000ms default/);
|
|
69
71
|
} finally {
|
|
70
72
|
console.warn = originalWarn;
|
|
71
73
|
}
|
|
@@ -112,6 +114,28 @@ describe("loadConfig", () => {
|
|
|
112
114
|
assert.strictEqual(config.reviewEnabled, true);
|
|
113
115
|
});
|
|
114
116
|
|
|
117
|
+
it("loads hermesMemory from settings.json with precedence over the legacy config", () => {
|
|
118
|
+
fs.mkdirSync(path.dirname(TEST_CONFIG_PATH), { recursive: true });
|
|
119
|
+
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({
|
|
120
|
+
llmThinkingOverride: "high",
|
|
121
|
+
consolidationTimeoutMs: 240000,
|
|
122
|
+
}));
|
|
123
|
+
fs.writeFileSync(TEST_SETTINGS_PATH, JSON.stringify({
|
|
124
|
+
theme: "dark",
|
|
125
|
+
hermesMemory: {
|
|
126
|
+
llmThinkingOverride: "off",
|
|
127
|
+
llmModelOverride: " tokenin/deepseek-v4.1-flash ",
|
|
128
|
+
consolidationTimeoutMs: 300000,
|
|
129
|
+
},
|
|
130
|
+
}));
|
|
131
|
+
|
|
132
|
+
const config = loadConfig(TEST_CONFIG_PATH, TEST_SETTINGS_PATH);
|
|
133
|
+
assert.strictEqual(config.llmThinkingOverride, "off");
|
|
134
|
+
assert.strictEqual(config.llmModelOverride, "tokenin/deepseek-v4.1-flash");
|
|
135
|
+
assert.strictEqual(config.consolidationTimeoutMs, 300000);
|
|
136
|
+
assert.strictEqual(config.memoryMode, "policy-only");
|
|
137
|
+
});
|
|
138
|
+
|
|
115
139
|
it("only accepts boolean quickCheckOnOpen overrides", () => {
|
|
116
140
|
fs.mkdirSync(path.dirname(TEST_CONFIG_PATH), { recursive: true });
|
|
117
141
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({ quickCheckOnOpen: "false" }));
|
|
@@ -631,6 +631,7 @@ describe("registerConsolidateCommand", () => {
|
|
|
631
631
|
it("includes project memory when a project store is available", async () => {
|
|
632
632
|
let handler: any;
|
|
633
633
|
const notifications: string[] = [];
|
|
634
|
+
const statuses: Array<[string, string | undefined]> = [];
|
|
634
635
|
let projectReloaded = false;
|
|
635
636
|
|
|
636
637
|
const pi = {
|
|
@@ -655,7 +656,10 @@ describe("registerConsolidateCommand", () => {
|
|
|
655
656
|
registerConsolidateCommand(pi, mockStore, 60000, projectStore, "demo-project");
|
|
656
657
|
await handler({}, {
|
|
657
658
|
signal: undefined,
|
|
658
|
-
ui: {
|
|
659
|
+
ui: {
|
|
660
|
+
notify: (message: string) => { notifications.push(message); },
|
|
661
|
+
setStatus: (key: string, text: string | undefined) => { statuses.push([key, text]); },
|
|
662
|
+
},
|
|
659
663
|
});
|
|
660
664
|
|
|
661
665
|
assert.strictEqual(execCalls.length, 4, "should consolidate memory, user, failure, and project stores");
|
|
@@ -670,6 +674,8 @@ describe("registerConsolidateCommand", () => {
|
|
|
670
674
|
assert.ok(projectReloaded, "project store should reload after consolidation");
|
|
671
675
|
assert.ok(notifications.some((message) => message.includes("Starting memory consolidation")), "should show an initial progress notification");
|
|
672
676
|
assert.ok(notifications.some((message) => message.includes("⏳ Consolidating memory")), "should show per-target progress");
|
|
677
|
+
assert.ok(statuses.some(([key, text]) => key === "pi-hermes-memory:consolidation" && text?.includes("Consolidating memory")), "should publish busy progress");
|
|
678
|
+
assert.deepStrictEqual(statuses.at(-1), ["pi-hermes-memory:consolidation", undefined], "should clear busy progress when done");
|
|
673
679
|
const finalNotification = notifications[notifications.length - 1] ?? "";
|
|
674
680
|
assert.ok(finalNotification.includes("failure: ✅ consolidated"), "final notification should include failure result");
|
|
675
681
|
assert.ok(finalNotification.includes("project:demo-project: ✅ consolidated"), "final notification should include project result");
|
|
@@ -1186,7 +1186,11 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
1186
1186
|
const deliveredEntry = { ...entry, message: injectedMessage, replyCommand };
|
|
1187
1187
|
replyTracker.queueTurnContext({ from: entry.from, message: injectedMessage, receivedAt: Date.now() });
|
|
1188
1188
|
const senderDisplay = entry.from.name || entry.from.id.slice(0, 8);
|
|
1189
|
-
|
|
1189
|
+
// The tool may be an inactive optional capability: say how to activate it, not just its call.
|
|
1190
|
+
const activateHint = pi.getActiveTools().includes("intercom")
|
|
1191
|
+
? ""
|
|
1192
|
+
: ' (not active yet: call capability_discover with name "intercom" first)';
|
|
1193
|
+
const replyInstruction = replyCommand ? `\n\nTo reply, use the intercom tool${activateHint}: ${replyCommand}` : "";
|
|
1190
1194
|
const deliveryMetadata = formatInboundDeliveryMetadata(injectedMessage);
|
|
1191
1195
|
pi.sendMessage(
|
|
1192
1196
|
{
|
|
@@ -41,9 +41,11 @@ esac
|
|
|
41
41
|
function makePi() {
|
|
42
42
|
const handlers = new Map<string, Function[]>();
|
|
43
43
|
const pi = {
|
|
44
|
-
exec: async (cmd: string, args: string[]
|
|
44
|
+
exec: async (cmd: string, args: string[]) =>
|
|
45
45
|
new Promise((resolve) => {
|
|
46
|
-
|
|
46
|
+
// The shim runs under the suite's own load: enforcing the extension's production
|
|
47
|
+
// 2s budget here kills it, which the extension reports as a failed probe.
|
|
48
|
+
execFile(cmd, args, (error, stdout, stderr) => {
|
|
47
49
|
if (error) {
|
|
48
50
|
// execFile reports non-zero exits as errors; keep the real exit code.
|
|
49
51
|
const code =
|
|
@@ -63,8 +65,14 @@ function makePi() {
|
|
|
63
65
|
return { pi: pi as any, handlers };
|
|
64
66
|
}
|
|
65
67
|
|
|
68
|
+
// Registration is deferred off startup (the managed-binary probe may spawn a process),
|
|
69
|
+
// so hook installation can outlast vi.waitFor's 1s default on a loaded machine.
|
|
70
|
+
function waitFor<T>(assertion: () => T | Promise<T>): Promise<T> {
|
|
71
|
+
return vi.waitFor(assertion, { timeout: 5_000 });
|
|
72
|
+
}
|
|
73
|
+
|
|
66
74
|
async function getToolCall(handlers: Map<string, Function[]>): Promise<Function> {
|
|
67
|
-
await
|
|
75
|
+
await waitFor(() => expect(handlers.has("tool_call")).toBe(true));
|
|
68
76
|
return handlers.get("tool_call")![0]!;
|
|
69
77
|
}
|
|
70
78
|
|
|
@@ -131,7 +139,7 @@ posixOnly("rtk extension", () => {
|
|
|
131
139
|
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
132
140
|
const { pi, handlers } = makePi();
|
|
133
141
|
rtkExtension(pi);
|
|
134
|
-
await
|
|
142
|
+
await waitFor(() => expect(warn).toHaveBeenCalledWith(expect.stringContaining("rtk gain failed")));
|
|
135
143
|
expect(handlers.has("tool_call")).toBe(false);
|
|
136
144
|
} finally {
|
|
137
145
|
if (previousPath) process.env.PATH = previousPath;
|
|
@@ -156,7 +164,7 @@ posixOnly("rtk extension", () => {
|
|
|
156
164
|
// Override exec so --version fails.
|
|
157
165
|
pi.exec = vi.fn(async () => ({ code: 1, stdout: "", stderr: "not found", killed: false }));
|
|
158
166
|
rtkExtension(pi);
|
|
159
|
-
await
|
|
167
|
+
await waitFor(() => expect(warn).toHaveBeenCalledWith(expect.stringContaining("failed --version")));
|
|
160
168
|
expect(handlers.has("tool_call")).toBe(false);
|
|
161
169
|
});
|
|
162
170
|
|
|
@@ -166,7 +174,7 @@ posixOnly("rtk extension", () => {
|
|
|
166
174
|
ensureToolMock.mockResolvedValue("rtk");
|
|
167
175
|
pi.exec = vi.fn(async () => ({ code: 0, stdout: "garbage output", stderr: "", killed: false }));
|
|
168
176
|
rtkExtension(pi);
|
|
169
|
-
await
|
|
177
|
+
await waitFor(() => expect(warn).toHaveBeenCalledWith(expect.stringContaining("could not parse version")));
|
|
170
178
|
expect(handlers.has("tool_call")).toBe(false);
|
|
171
179
|
});
|
|
172
180
|
|
|
@@ -176,7 +184,7 @@ posixOnly("rtk extension", () => {
|
|
|
176
184
|
ensureToolMock.mockResolvedValue("rtk");
|
|
177
185
|
pi.exec = vi.fn(async () => ({ code: 0, stdout: " ", stderr: "", killed: false }));
|
|
178
186
|
rtkExtension(pi);
|
|
179
|
-
await
|
|
187
|
+
await waitFor(() => expect(warn).toHaveBeenCalledWith(expect.stringContaining("<empty output>")));
|
|
180
188
|
expect(handlers.has("tool_call")).toBe(false);
|
|
181
189
|
});
|
|
182
190
|
|
|
@@ -186,7 +194,7 @@ posixOnly("rtk extension", () => {
|
|
|
186
194
|
ensureToolMock.mockResolvedValue("rtk");
|
|
187
195
|
pi.exec = vi.fn(async () => ({ code: 0, stdout: "rtk 0.22.0", stderr: "", killed: false }));
|
|
188
196
|
rtkExtension(pi);
|
|
189
|
-
await
|
|
197
|
+
await waitFor(() => expect(warn).toHaveBeenCalledWith(expect.stringContaining("too old")));
|
|
190
198
|
expect(handlers.has("tool_call")).toBe(false);
|
|
191
199
|
});
|
|
192
200
|
|
|
@@ -195,7 +203,7 @@ posixOnly("rtk extension", () => {
|
|
|
195
203
|
ensureToolMock.mockRejectedValue(new Error("download failed"));
|
|
196
204
|
const { pi, handlers } = makePi();
|
|
197
205
|
rtkExtension(pi);
|
|
198
|
-
await
|
|
206
|
+
await waitFor(() => expect(warn).toHaveBeenCalledWith(expect.stringContaining("managed installation failed")));
|
|
199
207
|
expect(handlers.has("tool_call")).toBe(false);
|
|
200
208
|
});
|
|
201
209
|
|
|
@@ -204,7 +212,7 @@ posixOnly("rtk extension", () => {
|
|
|
204
212
|
ensureToolMock.mockRejectedValue("plain string failure");
|
|
205
213
|
const { pi, handlers } = makePi();
|
|
206
214
|
rtkExtension(pi);
|
|
207
|
-
await
|
|
215
|
+
await waitFor(() => expect(warn).toHaveBeenCalledWith(expect.stringContaining("plain string failure")));
|
|
208
216
|
expect(handlers.has("tool_call")).toBe(false);
|
|
209
217
|
});
|
|
210
218
|
|
|
@@ -213,7 +221,7 @@ posixOnly("rtk extension", () => {
|
|
|
213
221
|
ensureToolMock.mockResolvedValue(undefined);
|
|
214
222
|
const { pi, handlers } = makePi();
|
|
215
223
|
rtkExtension(pi);
|
|
216
|
-
await
|
|
224
|
+
await waitFor(() => expect(warn).toHaveBeenCalledWith(expect.stringContaining("unavailable")));
|
|
217
225
|
expect(handlers.has("tool_call")).toBe(false);
|
|
218
226
|
});
|
|
219
227
|
|
|
@@ -225,7 +233,7 @@ posixOnly("rtk extension", () => {
|
|
|
225
233
|
throw new Error("spawn ENOENT");
|
|
226
234
|
});
|
|
227
235
|
rtkExtension(pi);
|
|
228
|
-
await
|
|
236
|
+
await waitFor(() => expect(warn).toHaveBeenCalledWith(expect.stringContaining("verification failed")));
|
|
229
237
|
expect(handlers.has("tool_call")).toBe(false);
|
|
230
238
|
});
|
|
231
239
|
|
|
@@ -237,7 +245,7 @@ posixOnly("rtk extension", () => {
|
|
|
237
245
|
throw "string failure";
|
|
238
246
|
});
|
|
239
247
|
rtkExtension(pi);
|
|
240
|
-
await
|
|
248
|
+
await waitFor(() => expect(warn).toHaveBeenCalledWith(expect.stringContaining("string failure")));
|
|
241
249
|
expect(handlers.has("tool_call")).toBe(false);
|
|
242
250
|
});
|
|
243
251
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { calculateLiveTps, calculateReliableTps, type TpsTiming } from "./tps.ts";
|
|
2
|
+
import { calculateLiveTps, calculateReliableTps, setupTpsTracker, type TpsTiming } from "./tps.ts";
|
|
3
3
|
|
|
4
4
|
function timing(overrides: Partial<TpsTiming> = {}): TpsTiming {
|
|
5
5
|
return {
|
|
@@ -23,6 +23,37 @@ describe("calculateLiveTps", () => {
|
|
|
23
23
|
});
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
+
describe("setupTpsTracker with anthropic-style usage", () => {
|
|
27
|
+
it("ignores the tiny usage.output seeded at message_start while streaming", async () => {
|
|
28
|
+
const handlers = new Map<string, (event: any, ctx: any) => Promise<void>>();
|
|
29
|
+
const statuses: string[] = [];
|
|
30
|
+
const ctx = { ui: { setStatus: (_k: string, v: string) => statuses.push(v), notify: () => {}, theme: { fg: (_c: string, t: string) => t } } };
|
|
31
|
+
setupTpsTracker({ on: (name: string, fn: any) => handlers.set(name, fn) } as any);
|
|
32
|
+
|
|
33
|
+
let now = 0;
|
|
34
|
+
const realNow = performance.now;
|
|
35
|
+
performance.now = () => now;
|
|
36
|
+
try {
|
|
37
|
+
const message = { role: "assistant", provider: "anthropic", model: "claude", usage: { output: 1 } };
|
|
38
|
+
await handlers.get("agent_start")!({}, ctx);
|
|
39
|
+
await handlers.get("message_start")!({ message }, ctx);
|
|
40
|
+
for (let i = 0; i < 20; i++) {
|
|
41
|
+
now += 50;
|
|
42
|
+
await handlers.get("message_update")!({ message, assistantMessageEvent: { type: "text_delta", delta: "x".repeat(40) } }, ctx);
|
|
43
|
+
}
|
|
44
|
+
// 20 deltas * 10 est. tokens over ~1s => ~200 tok/s, not ~1 tok/s
|
|
45
|
+
expect(statuses.at(-1)).toBe("211 tok/s");
|
|
46
|
+
|
|
47
|
+
message.usage.output = 200;
|
|
48
|
+
await handlers.get("message_end")!({ message }, ctx);
|
|
49
|
+
await handlers.get("agent_end")!({}, ctx);
|
|
50
|
+
expect(statuses.at(-1)).toMatch(/^done [1-9]\d* t\/s \(main [1-9]\d* t\/s\)$/);
|
|
51
|
+
} finally {
|
|
52
|
+
performance.now = realNow;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
26
57
|
describe("calculateReliableTps", () => {
|
|
27
58
|
it("uses active stream time for sufficiently sampled output", () => {
|
|
28
59
|
expect(calculateReliableTps(100, timing())).toEqual({
|
package/dist/extensions/tps.ts
CHANGED
|
@@ -230,7 +230,9 @@ export function setupTpsTracker(pi: ExtensionAPI): void {
|
|
|
230
230
|
streamStart ??= now;
|
|
231
231
|
estimatedStreamedTokens += Math.max(0, streamEvent.delta.length / 4);
|
|
232
232
|
const officialTokens = generatedTokensFromUsage(asRecord(event.message.usage));
|
|
233
|
-
|
|
233
|
+
// Anthropic seeds usage.output (~1) at message_start and only finalizes it at message_delta,
|
|
234
|
+
// so a nonzero official count mid-stream is not authoritative; take whichever is larger.
|
|
235
|
+
const currentTokens = Math.max(officialTokens, estimatedStreamedTokens);
|
|
234
236
|
const tps = calculateLiveTps(currentTokens, now - streamStart);
|
|
235
237
|
if (tps !== null) {
|
|
236
238
|
ctx.ui.setStatus("tps", ctx.ui.theme.fg("accent", `${tps} tok/s`));
|
package/docs/settings.md
CHANGED
|
@@ -49,8 +49,8 @@ Use `/trust` in interactive mode to save a project trust decision for future ses
|
|
|
49
49
|
### Jev Advisory Routing
|
|
50
50
|
|
|
51
51
|
The bundled `jev-advisory-routing` extension uses the Jev decisions model for bounded opt-in
|
|
52
|
-
routing.
|
|
53
|
-
Jev provider/model pair:
|
|
52
|
+
routing. The host-side routes stay off until they are enabled in `jevAdvisory`; the agent's own
|
|
53
|
+
`ask` route is on by default. All three share one Jev provider/model pair:
|
|
54
54
|
|
|
55
55
|
- `memory` — only after an explicit durable-memory cue (for example “the convention we
|
|
56
56
|
decided” or “don't repeat the past failure”), Jev chooses one read-only local
|
|
@@ -58,6 +58,14 @@ Jev provider/model pair:
|
|
|
58
58
|
- `recommendations` — one discovered skill or prompt workflow that fits, or a proportionate
|
|
59
59
|
verification level. Nothing is loaded, started, or executed: the recommendation is context for the
|
|
60
60
|
agent, and required project/workflow gates are unchanged.
|
|
61
|
+
- `ask` — the agent-callable `ask_jev` tool (on by default). The agent decides when to call it: it
|
|
62
|
+
passes its own prose, file `paths`, and one `command`, and gets typed choice/score/noul answers back —
|
|
63
|
+
never the file contents or command output, so a verdict about code costs a few lines of context
|
|
64
|
+
instead of the files. Files must be inside the working directory, secret-named files (`.env`,
|
|
65
|
+
`*.pem`, `id_rsa*`, `auth.json`, …) are refused, and the command runs through the bash tool's local
|
|
66
|
+
shell. Whatever the agent passes is sent to Jev. Without Token-In credentials the tool is taken out
|
|
67
|
+
of the agent's loadout before each run (it returns after `/tokenin add`, no reload needed), and a call
|
|
68
|
+
that slips through reads no file and runs no command.
|
|
61
69
|
|
|
62
70
|
| Setting | Type | Default | Description |
|
|
63
71
|
|---------|------|---------|-------------|
|
|
@@ -66,11 +74,15 @@ Jev provider/model pair:
|
|
|
66
74
|
| `jevAdvisory.baseUrl` | string | inherited | Base URL override; defaults to any registered model of `provider` |
|
|
67
75
|
| `jevAdvisory.routes.memory.enabled` | boolean | `false` | Enable the memory-lookup route |
|
|
68
76
|
| `jevAdvisory.routes.recommendations.enabled` | boolean | `false` | Enable skill/workflow and verification recommendations |
|
|
69
|
-
|
|
|
77
|
+
| `jevAdvisory.routes.ask.enabled` | boolean | `true` | Offer the agent the `ask_jev` tool; set `false` to turn it off |
|
|
78
|
+
| `<route>.timeoutMs` | number | `8000` | Route request timeout (ms); memory is capped at 750ms; `ask` defaults to `15000` |
|
|
70
79
|
| `<route>.minConfidence` | number | `0.6` | Below this Jev confidence the route abstains |
|
|
71
80
|
| `<route>.contextTurns` | number | `4` | Prior user turns sent as recommendation context; memory sends only the current bounded prompt |
|
|
72
81
|
| `<route>.contextChars` | number | `4000` | Character budget for recommendation context |
|
|
73
|
-
| `<route>.payloadBytes` | number | `8192` | Hard cap on the serialized decision request |
|
|
82
|
+
| `<route>.payloadBytes` | number | `8192` | Hard cap on the serialized decision request; `ask` defaults to `32768` |
|
|
83
|
+
|
|
84
|
+
`ask` ignores `minConfidence`, `contextTurns`, and `contextChars`: it returns every confidence to the
|
|
85
|
+
agent, and its context is whatever the agent put in the request.
|
|
74
86
|
|
|
75
87
|
Only idle, top-level, interactive prompts are routed: queued steering/follow-up input, slash
|
|
76
88
|
commands, and extension-injected turns are skipped, and each turn is routed at most once. The memory
|
|
@@ -99,16 +111,28 @@ needed: a compact `capability_catalog` lists them, `capability_discover` activat
|
|
|
99
111
|
current run, and `capability_skill_show` loads one skill's full instructions. Set
|
|
100
112
|
`SELESAI_CAPABILITY_GATEWAY=0` to disable the gateway and keep every tool visible.
|
|
101
113
|
|
|
102
|
-
Routing has
|
|
114
|
+
Routing has three rungs:
|
|
103
115
|
|
|
104
116
|
1. A deterministic router activates a tool when the prompt uniquely matches its name, alias, or
|
|
105
117
|
discovery summary. Skills are never auto-loaded or auto-selected.
|
|
106
118
|
2. Default-on Jev tie-breaking: when the deterministic router returns an ambiguous lexical hint
|
|
107
119
|
among optional tools, the Jev decisions model is asked which of two or three hinted tools (or
|
|
108
120
|
`none`) should be exposed. Jev sees only the bounded current prompt and each hinted tool's compact
|
|
109
|
-
discovery line — never conversation history
|
|
121
|
+
discovery line — never conversation history or tool schemas. A prompt with no
|
|
110
122
|
lexical signal, a unique activation, and a skill-only match never reach Jev. Token-In credentials
|
|
111
123
|
are required; without them, no Jev request is sent and Selesai prompts you to run `/tokenin add`.
|
|
124
|
+
3. The agent-driven route: `capability_discover` also takes a `job` instead of a name. Tools and
|
|
125
|
+
skills are separate decisions — a tool is callable code from an extension or MCP server that the
|
|
126
|
+
agent may use many times, a skill is a written procedure it reads once — so one Jev request asks
|
|
127
|
+
two questions, each from catalog metadata only (never a schema or skill body), each allowed to
|
|
128
|
+
answer `none`. A chosen tool is activated for the run and its parameters are returned; a chosen
|
|
129
|
+
skill is named for `capability_skill_show`. Tools are offered first, so only skills can be left out
|
|
130
|
+
of an oversized catalog, and a `none` then says how many were not considered. A skill found in
|
|
131
|
+
several sources is offered once. While Jev has no credential, the `job` route is left out of the
|
|
132
|
+
agent's instructions and the agent is pointed at `capability_catalog` plus an exact `name`.
|
|
133
|
+
|
|
134
|
+
All Jev features share one warning per session, shown the first time one of them actually needs the
|
|
135
|
+
missing credential; a user without a subscription is not warned just for starting a session.
|
|
112
136
|
|
|
113
137
|
| Setting | Type | Default | Description |
|
|
114
138
|
|---------|------|---------|-------------|
|
|
@@ -117,8 +141,9 @@ Routing has two rungs:
|
|
|
117
141
|
| `capabilityGateway.routing.jev.model` | string | `"jev-1.13"` | Decisions model the tie-breaker calls |
|
|
118
142
|
| `capabilityGateway.routing.jev.baseUrl` | string | inherited | Base URL override; defaults to any registered model of `provider` |
|
|
119
143
|
| `capabilityGateway.routing.jev.timeoutMs` | number | `1000` | Pre-turn request timeout (ms); hard-capped at `2000` |
|
|
144
|
+
| `capabilityGateway.routing.jev.discoverTimeoutMs` | number | `5000` | Timeout (ms) for `capability_discover({ job })`; hard-capped at `15000` |
|
|
120
145
|
| `capabilityGateway.routing.jev.minConfidence` | number | `0.6` | Below this Jev confidence the tie-breaker abstains |
|
|
121
|
-
| `capabilityGateway.routing.jev.payloadBytes` | number | `
|
|
146
|
+
| `capabilityGateway.routing.jev.payloadBytes` | number | `32768` | Hard cap on the serialized decision request; the pre-turn tie-break uses a few hundred bytes of it |
|
|
122
147
|
|
|
123
148
|
This area is independent of `jevAdvisory`: gateway routing reads only
|
|
124
149
|
`capabilityGateway.routing.jev` and shares just the Jev provider/model deployment identity. A
|
|
@@ -362,6 +387,20 @@ When selesai reads extensions from both `~/.selesai/agent/extensions/` and `~/.p
|
|
|
362
387
|
|
|
363
388
|
Keys are top-level entry names (dir name for packaged extensions, file name for loose `.ts`). Values are `"selesai"` or `"pi"`. See [Shared Host Extensions](shared-host-extensions.md) for the full flow.
|
|
364
389
|
|
|
390
|
+
## Bundled extension settings
|
|
391
|
+
|
|
392
|
+
The `pi-hermes-memory` extension reads its options from the `hermesMemory` object in global `~/.selesai/agent/settings.json`:
|
|
393
|
+
|
|
394
|
+
```json
|
|
395
|
+
{
|
|
396
|
+
"hermesMemory": {
|
|
397
|
+
"llmThinkingOverride": "off",
|
|
398
|
+
"consolidationTimeoutMs": 300000
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
These settings load at startup and take precedence over the legacy `hermes-memory-config.json` fallback. `llmModelOverride` is optional and uses `provider/model` format (for example, `tokenin/deepseek-v4.1-flash` if your account has access); leave it unset to use the active model. See the [memory extension configuration reference](../src/extensions/pi-hermes-memory/README.md#configuration) for all options.
|
|
365
404
|
|
|
366
405
|
## Example
|
|
367
406
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@selesai/code",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.34",
|
|
4
4
|
"description": "Maintained, extension-first Pi coding agent with built-in workflows, subagents, web research, questions, skills, and an enhanced terminal UI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -55,8 +55,8 @@
|
|
|
55
55
|
"clean": "shx rm -rf dist",
|
|
56
56
|
"dev": "tsx src/cli.ts",
|
|
57
57
|
"dev:print": "tsx src/cli.ts --print",
|
|
58
|
-
"test": "vitest run src/__tests__/model-registry-completion.test.ts src/extensions/jev/decisions.test.ts src/extensions/capability-gateway/routing.test.ts src/core/settings-manager-auto-handoff.test.ts src/core/remote-catalog-provider.test.ts src/extensions/jev-advisory-memory.test.ts src/extensions/jev-advisory-recommendations.test.ts src/extensions/jev-advisory-lifecycle.test.ts src/extensions/undo.test.ts src/extensions/tps.test.ts src/extensions/pi-graft src/extensions/agent-browser.test.ts src/extensions/auto-session-name.test.ts src/extensions/context-compaction-reminder.test.ts src/extensions/copy-turn.test.ts src/extensions/grep-app/index.test.ts src/extensions/inline-skills.test.ts src/extensions/rtk.test.ts src/extensions/handoff-new.test.ts src/extensions/question/tests src/extensions/ponytail/test src/__tests__/tokenin-search.test.ts src/__tests__/tokenin-onboarding.test.ts src/__tests__/model-registry-defaults.test.ts src/core/system-prompt.test.ts src/core/session-format.test.ts test/settings-manager-compaction.test.ts test/suite/agent-session-runtime.test.ts test/suite/agent-session-prompt.test.ts test/suite/agent-session-queue.test.ts test/suite/agent-session-retry-events.test.ts test/suite/agent-session-compaction.test.ts test/suite/agent-session-compaction-model-overrides.test.ts test/suite/agent-session-bash-persistence.test.ts test/suite/agent-session-model-extension.test.ts test/clipboard.test.ts test/clipboard-command.test.ts test/clipboard-image.test.ts test/clipboard-image-bmp-conversion.test.ts test/clipboard-image-native-errors.test.ts src/cli/args.test.ts",
|
|
59
|
-
"test:coverage": "vitest run --coverage src/__tests__/model-registry-completion.test.ts src/extensions/jev/decisions.test.ts src/extensions/capability-gateway/routing.test.ts src/core/settings-manager-auto-handoff.test.ts src/core/remote-catalog-provider.test.ts src/extensions/jev-advisory-memory.test.ts src/extensions/jev-advisory-recommendations.test.ts src/extensions/jev-advisory-lifecycle.test.ts src/extensions/undo.test.ts src/extensions/tps.test.ts src/extensions/agent-browser.test.ts src/extensions/auto-session-name.test.ts src/extensions/context-compaction-reminder.test.ts src/extensions/copy-turn.test.ts src/extensions/grep-app/index.test.ts src/extensions/inline-skills.test.ts src/extensions/rtk.test.ts src/extensions/handoff-new.test.ts src/extensions/question/tests src/extensions/ponytail/test src/__tests__/tokenin-search.test.ts src/__tests__/tokenin-onboarding.test.ts src/__tests__/model-registry-defaults.test.ts src/core/system-prompt.test.ts src/core/session-format.test.ts test/settings-manager-compaction.test.ts test/suite/agent-session-runtime.test.ts test/suite/agent-session-prompt.test.ts test/suite/agent-session-queue.test.ts test/suite/agent-session-retry-events.test.ts test/suite/agent-session-compaction.test.ts test/suite/agent-session-compaction-model-overrides.test.ts test/suite/agent-session-bash-persistence.test.ts test/suite/agent-session-model-extension.test.ts test/clipboard.test.ts test/clipboard-command.test.ts test/clipboard-image.test.ts test/clipboard-image-bmp-conversion.test.ts test/clipboard-image-native-errors.test.ts src/cli/args.test.ts",
|
|
58
|
+
"test": "vitest run src/__tests__/model-registry-completion.test.ts src/extensions/jev/decisions.test.ts src/extensions/capability-gateway/routing.test.ts src/core/settings-manager-auto-handoff.test.ts src/core/remote-catalog-provider.test.ts src/extensions/jev-advisory-memory.test.ts src/extensions/jev-advisory-recommendations.test.ts src/extensions/jev-ask-tool.test.ts src/extensions/jev-advisory-lifecycle.test.ts src/extensions/undo.test.ts src/extensions/tps.test.ts src/extensions/pi-graft src/extensions/agent-browser.test.ts src/extensions/auto-session-name.test.ts src/extensions/context-compaction-reminder.test.ts src/extensions/copy-turn.test.ts src/extensions/grep-app/index.test.ts src/extensions/inline-skills.test.ts src/extensions/rtk.test.ts src/extensions/handoff-new.test.ts src/extensions/question/tests src/extensions/ponytail/test src/__tests__/tokenin-search.test.ts src/__tests__/tokenin-onboarding.test.ts src/__tests__/model-registry-defaults.test.ts src/core/system-prompt.test.ts src/core/session-format.test.ts test/settings-manager-compaction.test.ts test/suite/agent-session-runtime.test.ts test/suite/agent-session-prompt.test.ts test/suite/agent-session-queue.test.ts test/suite/agent-session-retry-events.test.ts test/suite/agent-session-compaction.test.ts test/suite/agent-session-compaction-model-overrides.test.ts test/suite/agent-session-bash-persistence.test.ts test/suite/agent-session-model-extension.test.ts test/clipboard.test.ts test/clipboard-command.test.ts test/clipboard-image.test.ts test/clipboard-image-bmp-conversion.test.ts test/clipboard-image-native-errors.test.ts src/cli/args.test.ts",
|
|
59
|
+
"test:coverage": "vitest run --coverage src/__tests__/model-registry-completion.test.ts src/extensions/jev/decisions.test.ts src/extensions/capability-gateway/routing.test.ts src/core/settings-manager-auto-handoff.test.ts src/core/remote-catalog-provider.test.ts src/extensions/jev-advisory-memory.test.ts src/extensions/jev-advisory-recommendations.test.ts src/extensions/jev-ask-tool.test.ts src/extensions/jev-advisory-lifecycle.test.ts src/extensions/undo.test.ts src/extensions/tps.test.ts src/extensions/agent-browser.test.ts src/extensions/auto-session-name.test.ts src/extensions/context-compaction-reminder.test.ts src/extensions/copy-turn.test.ts src/extensions/grep-app/index.test.ts src/extensions/inline-skills.test.ts src/extensions/rtk.test.ts src/extensions/handoff-new.test.ts src/extensions/question/tests src/extensions/ponytail/test src/__tests__/tokenin-search.test.ts src/__tests__/tokenin-onboarding.test.ts src/__tests__/model-registry-defaults.test.ts src/core/system-prompt.test.ts src/core/session-format.test.ts test/settings-manager-compaction.test.ts test/suite/agent-session-runtime.test.ts test/suite/agent-session-prompt.test.ts test/suite/agent-session-queue.test.ts test/suite/agent-session-retry-events.test.ts test/suite/agent-session-compaction.test.ts test/suite/agent-session-compaction-model-overrides.test.ts test/suite/agent-session-bash-persistence.test.ts test/suite/agent-session-model-extension.test.ts test/clipboard.test.ts test/clipboard-command.test.ts test/clipboard-image.test.ts test/clipboard-image-bmp-conversion.test.ts test/clipboard-image-native-errors.test.ts src/cli/args.test.ts",
|
|
60
60
|
"prepare": "npm run build",
|
|
61
61
|
"build": "npm run clean && tsgo -p tsconfig.build.json && shx chmod +x dist/cli.js dist/rpc-entry.js && npm run copy-assets",
|
|
62
62
|
"copy-assets": "shx mkdir -p dist/modes/interactive/theme && shx cp src/modes/interactive/theme/*.json dist/modes/interactive/theme/ && shx mkdir -p dist/modes/interactive/assets && shx cp src/modes/interactive/assets/*.png dist/modes/interactive/assets/ && shx mkdir -p dist/core/export-html/vendor && shx cp src/core/export-html/template.html src/core/export-html/template.css src/core/export-html/template.js dist/core/export-html/ && shx cp src/core/export-html/vendor/*.js dist/core/export-html/vendor/ && shx mkdir -p dist/defaults && shx cp src/defaults/* dist/defaults/ && shx mkdir -p dist/extensions && node scripts/copy-extensions.mjs && shx mkdir -p dist/themes && shx cp -r src/themes/. dist/themes/ && shx mkdir -p dist/skills && shx cp -r src/skills/. dist/skills/"
|