tr-plugin-meta-harness 0.0.0-stage → 0.3.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.
@@ -0,0 +1,446 @@
1
+ /**
2
+ * DSH CLI — query DeepSeek Harness state from Total Recall.
3
+ * Ran as `total-recall cli-agents runtime dsh <subcommand>`
4
+ * @module @total-recall/plugin-dsh
5
+ */
6
+
7
+ import { spawnSync } from 'node:child_process';
8
+ import fs from 'node:fs';
9
+ import os from 'node:os';
10
+ import path from 'node:path';
11
+
12
+ // The DSH Web/desktop GUI binds an EPHEMERAL port chosen at launch (observed
13
+ // 49661 -> 49703 -> 52406 within one session), so no fixed port list can ever
14
+ // be correct. Earlier revisions hardcoded [53626, 54866, 54055] and therefore
15
+ // reported "DeepSeek Harness is not running" against a live GUI.
16
+ //
17
+ // Discover by PROCESS IDENTITY instead: find the DSH process, then read the
18
+ // port it is actually listening on. The debugger port (9229/9230) is excluded.
19
+ const DSH_RUNTIME = process.env.DSH_RUNTIME || defaultRuntime();
20
+ const DEBUG_PORTS = new Set([9229, 9230]);
21
+ /** Process-name fragments that identify a DSH surface. */
22
+ const DSH_PROCESS_HINTS = ['Harness Dev.app', 'dsh-desktop', '@deepseek-ai/dsh'];
23
+
24
+ /** Locate the DSH checkout when DSH_RUNTIME is unset. */
25
+ function defaultRuntime() {
26
+ const candidates = [
27
+ process.env.DSH_CHECKOUT,
28
+ path.join(os.homedir(), 'github', 'deepseek-harness'),
29
+ path.join(os.homedir(), 'deepseek-harness')
30
+ ].filter(Boolean);
31
+ for (const dir of candidates) {
32
+ if (fs.existsSync(path.join(dir, 'packages'))) return dir;
33
+ }
34
+ return undefined;
35
+ }
36
+
37
+ /** PIDs whose command line looks like a DSH surface. */
38
+ function dshPids() {
39
+ const res = spawnSync('ps', ['-Ao', 'pid=,command='], { timeout: 4000, encoding: 'utf8' });
40
+ if (res.status !== 0 || !res.stdout) return [];
41
+ const pids = [];
42
+ for (const line of res.stdout.split('\n')) {
43
+ const trimmed = line.trim();
44
+ if (!trimmed) continue;
45
+ const match = /^(\d+)\s+(.*)$/.exec(trimmed);
46
+ if (!match) continue;
47
+ const [, pid, command] = match;
48
+ // Skip the CLI itself and any grep-like helper.
49
+ if (command.includes('tr-plugin-dsh') || command.includes('lsof') || command.includes('ps -Ao')) continue;
50
+ if (DSH_PROCESS_HINTS.some((hint) => command.includes(hint))) pids.push(Number(pid));
51
+ }
52
+ return pids;
53
+ }
54
+
55
+ /** Listening ports held by the given PIDs, debugger ports excluded. */
56
+ function listenPorts(pids) {
57
+ const ports = [];
58
+ for (const pid of pids) {
59
+ const res = spawnSync('lsof', ['-nP', '-iTCP', '-sTCP:LISTEN', '-a', '-p', String(pid)],
60
+ { timeout: 4000, encoding: 'utf8' });
61
+ if (res.status !== 0 || !res.stdout) continue;
62
+ for (const line of res.stdout.split('\n').slice(1)) {
63
+ // lsof columns end with `TCP host:port (LISTEN)`; match the address form
64
+ // directly rather than assuming the port is the last field, which yields
65
+ // the literal '(LISTEN)'.
66
+ const match = /TCP\s+\S*:(\d+)\s+\(LISTEN\)/.exec(line);
67
+ if (!match) continue;
68
+ const port = Number(match[1]);
69
+ if (port && !DEBUG_PORTS.has(port) && !ports.includes(port)) ports.push(port);
70
+ }
71
+ }
72
+ return ports;
73
+ }
74
+
75
+ /**
76
+ * Is this port the DSH Web/desktop GUI?
77
+ *
78
+ * The GUI is not always open: it answers with an authentication notice
79
+ * ("dsh web authentication required; reopen the URL printed by dsh web") rather
80
+ * than the boot payload, so probing for `__DSH_BOOT__` alone would reject the
81
+ * real GUI and fall through to the Electron devtools port.
82
+ */
83
+ function servesDsh(port) {
84
+ if (fetchBootInfo(port) !== null) return true;
85
+ const res = spawnSync('curl', ['-s', '-m', '4', `http://127.0.0.1:${port}/`],
86
+ { timeout: 5000, encoding: 'utf8' });
87
+ return res.status === 0 && typeof res.stdout === 'string' && res.stdout.includes('dsh web');
88
+ }
89
+
90
+ function findDsh() {
91
+ const pids = dshPids();
92
+ if (pids.length === 0) return null;
93
+
94
+ // Prefer a PID whose port actually serves the DSH boot payload; fall back to
95
+ // the first PID that merely holds a listening socket.
96
+ let fallback = null;
97
+ for (const pid of pids) {
98
+ for (const port of listenPorts([pid])) {
99
+ const entry = () => {
100
+ const cmd = spawnSync('ps', ['-p', String(pid), '-o', 'comm='], { timeout: 2000, encoding: 'utf8' });
101
+ const startTime = spawnSync('ps', ['-p', String(pid), '-o', 'lstart='], { timeout: 2000, encoding: 'utf8' });
102
+ return {
103
+ pid,
104
+ port,
105
+ name: (cmd.stdout || '').toString().trim(),
106
+ started: (startTime.stdout || '').toString().trim()
107
+ };
108
+ };
109
+ if (servesDsh(port)) return entry();
110
+ fallback ??= entry();
111
+ }
112
+ }
113
+ return fallback;
114
+ }
115
+
116
+ // --- Model configuration -----------------------------------------------------
117
+ //
118
+ // The configured default model lives in each profile's cordis.patch.yml under
119
+ // `- id: agent-default-model`. DSH keeps profiles in more than one home: the
120
+ // shared ~/.dsh and the desktop app's isolated build home. Reading only one is
121
+ // how a stale default (V3) stayed invisible in the others.
122
+
123
+ const DEFAULT_MODEL_PREF = path.join(os.homedir(), '.dsh', 'default-model.txt');
124
+
125
+ /** Every DSH home that holds profiles, in precedence order. */
126
+ function profileHomes() {
127
+ const homes = [path.join(os.homedir(), '.dsh')];
128
+ if (DSH_RUNTIME) {
129
+ homes.push(path.join(DSH_RUNTIME, 'apps', 'desktop', '.desktop-build', 'development', 'home'));
130
+ }
131
+ return homes;
132
+ }
133
+
134
+ /** Read the default model + provider from one profile patch. */
135
+ function readProfileDefault(patchFile) {
136
+ try {
137
+ const text = fs.readFileSync(patchFile, 'utf8');
138
+ const block = text.split('- id: agent-default-model')[1];
139
+ if (!block) return null;
140
+ const model = /^\s+model:\s*(\S+)/m.exec(block);
141
+ const provider = /^\s+provider:\s*(\S+)/m.exec(block);
142
+ if (!model) return null;
143
+ return { model: model[1], provider: provider ? provider[1] : 'unknown' };
144
+ } catch { return null; }
145
+ }
146
+
147
+ /** Every profile's configured default across all homes. */
148
+ function allProfileDefaults() {
149
+ const found = [];
150
+ for (const home of profileHomes()) {
151
+ const dir = path.join(home, 'profiles');
152
+ if (!fs.existsSync(dir)) continue;
153
+ for (const name of fs.readdirSync(dir)) {
154
+ const patch = path.join(dir, name, 'cordis.patch.yml');
155
+ if (!fs.existsSync(patch)) continue;
156
+ const def = readProfileDefault(patch);
157
+ if (def) found.push({ home: home.replace(os.homedir(), '~'), profile: name, ...def });
158
+ }
159
+ }
160
+ return found;
161
+ }
162
+
163
+ /** Catalogue cached by the model refresh script, when present. */
164
+ function cachedCatalogue() {
165
+ const file = path.join(os.homedir(), '.dsh', 'openrouter-models.json');
166
+ try {
167
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
168
+ const list = Array.isArray(parsed) ? parsed : parsed.data;
169
+ return Array.isArray(list) ? list : [];
170
+ } catch { return []; }
171
+ }
172
+
173
+ /** Provider packages compiled into the DSH tree. */
174
+ function providerPackages() {
175
+ const providers = [];
176
+ if (!DSH_RUNTIME) return providers;
177
+ const providerDir = path.join(DSH_RUNTIME, 'node_modules', '@deepseek-ai');
178
+ if (!fs.existsSync(providerDir)) return providers;
179
+ for (const pkg of fs.readdirSync(providerDir)) {
180
+ if (!(pkg.includes('llm') || pkg.includes('model') || pkg.includes('provider'))) continue;
181
+ const pkgPath = path.join(providerDir, pkg, 'package.json');
182
+ if (!fs.existsSync(pkgPath)) continue;
183
+ try {
184
+ const meta = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
185
+ providers.push({ name: meta.name, version: meta.version, desc: (meta.description || '').slice(0, 80) });
186
+ } catch { /* unreadable package */ }
187
+ }
188
+ return providers;
189
+ }
190
+
191
+ function calcUptime(started) {
192
+ if (!started) return 'unknown';
193
+ const start = new Date(started);
194
+ const now = new Date();
195
+ const diff = (now - start) / 1000;
196
+ const days = Math.floor(diff / 86400);
197
+ const hours = Math.floor((diff % 86400) / 3600);
198
+ const mins = Math.floor((diff % 3600) / 60);
199
+ const parts = [];
200
+ if (days > 0) parts.push(`${days}d`);
201
+ if (hours > 0) parts.push(`${hours}h`);
202
+ parts.push(`${mins}m`);
203
+ return parts.join(' ');
204
+ }
205
+
206
+ function fetchBootInfo(port) {
207
+ try {
208
+ const res = spawnSync('curl', ['-s', `http://127.0.0.1:${port}/`], { timeout: 5000, encoding: 'utf8' });
209
+ if (res.status !== 0) return null;
210
+ const html = res.stdout;
211
+ // Extract __DSH_BOOT__ JSON
212
+ const idx = html.indexOf('__DSH_BOOT__');
213
+ if (idx === -1) return null;
214
+ const brace = html.indexOf('{', idx);
215
+ let depth = 0, end = brace;
216
+ for (let i = brace; i < html.length; i++) {
217
+ if (html[i] === '{') depth++;
218
+ else if (html[i] === '}') depth--;
219
+ if (depth === 0) { end = i + 1; break; }
220
+ }
221
+ const boot = JSON.parse(html.slice(brace, end));
222
+ return boot;
223
+ } catch { return null; }
224
+ }
225
+
226
+ export async function run(argv) {
227
+ const subcommand = argv.includes('--help') || argv.includes('-h') ? 'help' : argv[3] || 'help';
228
+
229
+ const dsh = findDsh();
230
+ // `model`/`models` read profile files on disk, so they work without a running
231
+ // DSH; every other subcommand needs the live process.
232
+ const FILE_ONLY = new Set(['model', 'models']);
233
+ if (!dsh && subcommand !== 'help' && !FILE_ONLY.has(subcommand)) {
234
+ console.error(' DeepSeek Harness is not running.');
235
+ process.exitCode = 1;
236
+ return;
237
+ }
238
+
239
+ const bootInfo = dsh ? fetchBootInfo(dsh.port) : null;
240
+
241
+ switch (subcommand) {
242
+
243
+ case 'status': {
244
+ const mem = spawnSync('ps', ['-p', String(dsh.pid), '-o', 'rss='], { timeout: 2000 });
245
+ const rss = parseInt(mem.stdout.toString().trim()) || 0;
246
+ const uptime = calcUptime(dsh.started);
247
+
248
+ console.log(`\n DeepSeek Harness — Runtime\n`);
249
+ console.log(` PID: ${dsh.pid}`);
250
+ console.log(` Memory: ${(rss / 1024).toFixed(1)} MB`);
251
+ console.log(` Port: ${dsh.port}`);
252
+ console.log(` Process: ${dsh.name}`);
253
+ console.log(` Uptime: ${uptime}`);
254
+ console.log(` Revision: ${bootInfo?.rev || 'unknown'}`);
255
+ console.log(` Plugins: ${bootInfo?.entries?.length || 'unknown'} client plugins loaded`);
256
+ console.log(` Web GUI: ${process.env.DSH_WEB_URL || 'http://127.0.0.1:' + dsh.port}\n`);
257
+ return { data: { pid: dsh.pid, port: dsh.port, memory: (rss / 1024).toFixed(1), uptime, revision: bootInfo?.rev } };
258
+ }
259
+
260
+ case 'sessions': {
261
+ // DSH sessions are Cordis-managed. Report what we can observe externally
262
+ const elapsed = calcUptime(dsh.started);
263
+ const pluginCount = bootInfo?.entries?.length || 0;
264
+
265
+ console.log(`\n DeepSeek Harness — Sessions\n`);
266
+ console.log(` Status: Running since ${dsh.started} (${elapsed})`);
267
+ console.log(` Web GUI: ${process.env.DSH_WEB_URL || 'http://127.0.0.1:' + dsh.port}`);
268
+ console.log(` API Port: ${dsh.port}`);
269
+ console.log(` Revision: ${bootInfo?.rev || 'unknown'}`);
270
+ console.log(` Plugins: ${pluginCount} active`);
271
+ console.log(`\n Open the Web GUI to view and manage active sessions.`);
272
+ console.log(` Session state, conversation logs, and tool usage are`);
273
+ console.log(` available through the browser interface.\n`);
274
+ return { data: { status: 'running', port: dsh.port, uptime: elapsed, plugins: pluginCount, revision: bootInfo?.rev } };
275
+ }
276
+
277
+ case 'models': {
278
+ const providers = providerPackages();
279
+ const defaults = allProfileDefaults();
280
+ const catalogue = cachedCatalogue();
281
+
282
+ console.log(`\n DeepSeek Harness — Models\n`);
283
+
284
+ console.log(` Configured default model (per profile):\n`);
285
+ if (defaults.length === 0) {
286
+ console.log(` (no profile declares agent-default-model)\n`);
287
+ } else {
288
+ for (const d of defaults) {
289
+ console.log(` ${d.home}/${d.profile}`);
290
+ console.log(` model: ${d.model}`);
291
+ console.log(` provider: ${d.provider}`);
292
+ }
293
+ const distinct = [...new Set(defaults.map((d) => d.model))];
294
+ if (distinct.length > 1) {
295
+ console.log(`\n WARNING profiles disagree: ${distinct.join(', ')}`);
296
+ console.log(` A refresh that hardcodes a model id will revert these.`);
297
+ }
298
+ console.log();
299
+ }
300
+
301
+ let pref = '';
302
+ try { pref = fs.readFileSync(DEFAULT_MODEL_PREF, 'utf8').trim(); } catch { /* unset */ }
303
+ console.log(` Persisted preference: ${pref || '(unset)'}`);
304
+ console.log(` ${DEFAULT_MODEL_PREF}`);
305
+ console.log(` The model refresh keeps this model instead of a hardcoded id.\n`);
306
+
307
+ console.log(` Catalogue cache: ${catalogue.length} models`);
308
+ console.log(` ~/.dsh/openrouter-models.json\n`);
309
+
310
+ console.log(` Provider packages: ${providers.length}\n`);
311
+ for (const p of providers) {
312
+ console.log(` • ${p.name} v${p.version}`);
313
+ if (p.desc) console.log(` ${p.desc}`);
314
+ }
315
+ console.log(`\n Cache the catalogue for offline reads: total-recall cli-agents runtime dsh model --cache\n`);
316
+ return { data: { defaults, preference: pref || null, catalogueSize: catalogue.length, providers: providers.map((p) => p.name) } };
317
+ }
318
+
319
+ case 'model': {
320
+ // Model caching: read live configuration and (optionally) cache it.
321
+ const defaults = allProfileDefaults();
322
+ const catalogue = cachedCatalogue();
323
+ const asJson = argv.includes('--json');
324
+ const wantCache = argv.includes('--cache');
325
+
326
+ const payload = {
327
+ generatedAt: new Date().toISOString(),
328
+ runtime: DSH_RUNTIME || null,
329
+ preference: (() => {
330
+ try { return fs.readFileSync(DEFAULT_MODEL_PREF, 'utf8').trim(); } catch { return null; }
331
+ })(),
332
+ defaults,
333
+ catalogueSize: catalogue.length
334
+ };
335
+
336
+ if (wantCache) {
337
+ // Cache through SSSS memory so `total-recall recall` can answer model
338
+ // questions offline and on any mesh node.
339
+ const summary = [
340
+ `DSH model configuration cached ${payload.generatedAt}.`,
341
+ payload.preference ? `Persisted preference: ${payload.preference}.` : 'No persisted preference file.',
342
+ ...defaults.map((d) => `${d.home}/${d.profile} -> ${d.model} (${d.provider}).`),
343
+ `OpenRouter catalogue cache holds ${catalogue.length} models.`
344
+ ].join(' ');
345
+ const res = spawnSync('total-recall', [
346
+ 'remember', 'fact', summary,
347
+ '--title', 'DSH model configuration (cached)',
348
+ '--slug', 'dsh-model-configuration-cache',
349
+ '--tags', 'dsh,model,cache,openrouter',
350
+ '--global'
351
+ ], { timeout: 60000, encoding: 'utf8' });
352
+ payload.cached = res.status === 0;
353
+ payload.cacheOutput = (res.stdout || '').trim().split('\n').slice(-1)[0] || '';
354
+ if (res.status !== 0) {
355
+ console.error(` Cache write failed: ${(res.stderr || '').trim().split('\n')[0] || 'unknown error'}`);
356
+ }
357
+ }
358
+
359
+ if (asJson) {
360
+ console.log(JSON.stringify(payload, null, 2));
361
+ return { data: payload };
362
+ }
363
+
364
+ console.log(`\n DeepSeek Harness — Model Cache\n`);
365
+ console.log(` Preference: ${payload.preference || '(unset)'}`);
366
+ console.log(` Profiles: ${defaults.length}`);
367
+ for (const d of defaults) {
368
+ console.log(` • ${d.home}/${d.profile} -> ${d.model}`);
369
+ }
370
+ console.log(` Catalogue: ${catalogue.length} models cached`);
371
+ if (wantCache) {
372
+ console.log(` SSSS cache: ${payload.cached ? 'written' : 'FAILED'}`);
373
+ } else {
374
+ console.log(`\n Write the cache: total-recall cli-agents runtime dsh model --cache`);
375
+ }
376
+ console.log();
377
+ return { data: payload };
378
+ }
379
+
380
+ case 'plugins': {
381
+ const entries = bootInfo?.entries || [];
382
+
383
+ console.log(`\n DeepSeek Harness — Client Plugins\n`);
384
+ console.log(` Total: ${entries.length} plugins loaded\n`);
385
+ for (const e of entries) {
386
+ const deps = e.inject?.length ? ` [deps: ${e.inject.join(', ')}]` : '';
387
+ const imm = e.immediately ? ' [immediate]' : '';
388
+ console.log(` • ${e.id}${imm}${deps}`);
389
+ }
390
+ console.log(`\n Dynamic Cordis Plugins (from current session):`);
391
+ console.log(` Use cordis_inspect_self inside a DSH session to list them.\n`);
392
+ return { data: { pluginCount: entries.length, plugins: entries.map(e => e.id) } };
393
+ }
394
+
395
+ case 'tools': {
396
+ // Parse available plug-in packages for tool-related ones
397
+ const pluginDir = path.join(DSH_RUNTIME, 'node_modules', '@deepseek-ai');
398
+ const tools = [];
399
+ if (fs.existsSync(pluginDir)) {
400
+ for (const pkg of fs.readdirSync(pluginDir)) {
401
+ if (pkg.includes('tool') || pkg.includes('skill') || pkg.includes('command')) {
402
+ const pkgPath = path.join(pluginDir, pkg, 'package.json');
403
+ if (fs.existsSync(pkgPath)) {
404
+ try {
405
+ const meta = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
406
+ tools.push({ name: meta.name, version: meta.version, desc: (meta.description || '').slice(0, 100) });
407
+ } catch {}
408
+ }
409
+ }
410
+ }
411
+ }
412
+
413
+ // Known tool packages specific to DSH
414
+ const knownTools = []; // Only observed packages are reported; no fabricated live tool registry.
415
+
416
+ console.log(`\n DeepSeek Harness — Tool Registry\n`);
417
+ console.log(` Built-in Agent Tools:\n`);
418
+ for (const t of knownTools) {
419
+ console.log(` • ${t.name} — ${t.desc}`);
420
+ }
421
+
422
+ if (tools.length > 0) {
423
+ console.log(`\n Tool-related Packages:\n`);
424
+ for (const t of tools) {
425
+ console.log(` • ${t.name} v${t.version}`);
426
+ if (t.desc) console.log(` ${t.desc}`);
427
+ }
428
+ }
429
+ console.log(`\n To see the complete live tool registry:`);
430
+ console.log(` Use cordis_inspect_list inside a DSH session, or`);
431
+ console.log(` check the Settings → Plugins panel in the Web GUI.\n`);
432
+ return { data: { builtIn: knownTools.map(t => t.name), packages: tools.map(t => t.name) } };
433
+ }
434
+
435
+ default:
436
+ console.log(`\n DeepSeek Harness — Total Recall Plugin\n`);
437
+ console.log('Usage: total-recall cli-agents runtime dsh <subcommand>\n');
438
+ console.log(' dsh status DSH runtime status (PID, port, memory, uptime)');
439
+ console.log(' dsh sessions Session status and Web GUI info');
440
+ console.log(' dsh tools Registered tool inventory');
441
+ console.log(' dsh models Configured default model per profile + providers');
442
+ console.log(' dsh model Model cache (--cache writes an SSSS node, --json)');
443
+ console.log(' dsh plugins Client plugin inventory\n');
444
+ return;
445
+ }
446
+ }
package/src/alerts.mjs ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Operator Alerts Dispatcher
3
+ * Bridges to Total Recall alerts plugin or system notifications.
4
+ */
5
+
6
+ import { execFileSync } from 'node:child_process';
7
+
8
+ export async function notifyAlert(alert = {}) {
9
+ const { title = 'Meta Harness Notification', message = '', severity = 'info' } = alert;
10
+
11
+ // Attempt to invoke total-recall alerts if present
12
+ try {
13
+ const totalRecallBin = process.env.TOTAL_RECALL_BIN || 'total-recall';
14
+ execFileSync(totalRecallBin, ['alerts', 'send', '--title', title, '--message', message, '--severity', severity], {
15
+ stdio: 'pipe',
16
+ timeout: 5000
17
+ });
18
+ return { ok: true, dispatchedVia: 'total-recall-alerts' };
19
+ } catch {
20
+ // Fallback: console log without crashing
21
+ return { ok: true, dispatchedVia: 'fallback-logged', logged: { title, message, severity } };
22
+ }
23
+ }