total-recall-brain 3.47.4 → 3.48.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.
@@ -151,6 +151,10 @@
151
151
  "minimum": 1,
152
152
  "maximum": 600000
153
153
  },
154
+ "interactive": {
155
+ "type": "boolean",
156
+ "description": "The command is a terminal session (a remote shell, a sign-in). Run from the terminal CLI with stdin and stdout on a TTY, it is attached to the terminal with no runner timeout. Every other caller keeps piped output and the timeout."
157
+ },
154
158
  "args": {
155
159
  "type": "array",
156
160
  "items": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "total-recall-brain",
3
- "version": "3.47.4",
3
+ "version": "3.48.0",
4
4
  "description": "The Vibe Coding Brain & Autonomous Software Factory: memory, rules, secrets, and 33 visual plugins automatically managed across all your devices.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "tr-plugin-mesh",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "tr-plugin-mesh",
9
- "version": "0.3.0",
9
+ "version": "0.3.2",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@gregiteen/ssss-cli": "0.14.5",
@@ -15,7 +15,7 @@
15
15
  "devDependencies": {
16
16
  "jsdom": "^29.1.1",
17
17
  "supertest": "^7.3.1",
18
- "total-recall-brain": "3.47.0",
18
+ "total-recall-brain": "3.47.4",
19
19
  "vitest": "^4.1.12"
20
20
  }
21
21
  },
@@ -7177,9 +7177,9 @@
7177
7177
  }
7178
7178
  },
7179
7179
  "node_modules/total-recall-brain": {
7180
- "version": "3.47.0",
7181
- "resolved": "https://registry.npmjs.org/total-recall-brain/-/total-recall-brain-3.47.0.tgz",
7182
- "integrity": "sha512-S4UIdbbTyxw7C4+rix+7oHMm/6gYkcxwNBcP6kRDySX40p5IgqVTKgHk1n8rmaKiyYFoIX/HZHlHSjNrHgn0Gw==",
7180
+ "version": "3.47.4",
7181
+ "resolved": "https://registry.npmjs.org/total-recall-brain/-/total-recall-brain-3.47.4.tgz",
7182
+ "integrity": "sha512-XqUOHFfxiiMVCE7J6nGJdZNrh2M9HXEhIA9ZiKdrGab+AebxMzN5C8mmGVM1Wo1we9szHVUyYr6w+FHOEvni+A==",
7183
7183
  "dev": true,
7184
7184
  "license": "MIT",
7185
7185
  "workspaces": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tr-plugin-mesh",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "type": "module",
5
5
  "description": "Headscale and Tailscale SSH control-server mesh administration and node discovery",
6
6
  "scripts": {
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://github.com/gregiteen/total-recall/blob/main/metadata.plugin.schema.json",
3
3
  "id": "mesh",
4
4
  "name": "Headscale",
5
- "version": "0.3.1",
5
+ "version": "0.3.2",
6
6
  "description": "Control-server mesh administration, Tailscale SSH, and peer node discovery.",
7
7
  "license": "MIT",
8
8
  "reuse": {
@@ -166,8 +166,9 @@
166
166
  },
167
167
  {
168
168
  "name": "ssh",
169
- "description": "Mesh ssh operation",
170
- "effect": "write"
169
+ "description": "Open a terminal session on a mesh node, or run a command there",
170
+ "effect": "write",
171
+ "interactive": true
171
172
  },
172
173
  {
173
174
  "name": "exec",
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: mesh
3
3
  description: "Control-server mesh administration, Tailscale SSH, and peer node discovery."
4
- version: 0.3.0
4
+ version: 0.3.2
5
5
  repo_scoped: false
6
6
  ---
7
7
 
@@ -22,8 +22,8 @@ total-recall mesh status # Inspect live mesh state
22
22
  total-recall mesh view # Read independent owner observations for mesh screens
23
23
  total-recall mesh nodes # Mesh nodes operation
24
24
  total-recall mesh doctor # Mesh doctor operation
25
- total-recall mesh ssh # Mesh ssh operation
26
- total-recall mesh exec # Mesh exec operation
25
+ total-recall mesh ssh <node> [cmd] # Terminal session on a node, no time limit when run from a terminal
26
+ total-recall mesh exec <node> <cmd> # Scripted command (bounded by the runner timeout)
27
27
  total-recall mesh access # Mesh access operation
28
28
  total-recall mesh ping # Mesh ping operation
29
29
  total-recall mesh leader # Read the elected leader and its server port
@@ -31,6 +31,10 @@ total-recall mesh leader-pin # Change the pinned leader address and optional se
31
31
  total-recall mesh enroll # Mesh enroll operation
32
32
  ```
33
33
 
34
+ ## Terminal sessions
35
+
36
+ `ssh` is declared `interactive`. Run from a terminal (stdin and stdout on a TTY), Total Recall attaches it to that terminal with no runner timeout: window resizes and Ctrl-C reach ssh. A command given from a terminal gets a remote terminal (`ssh -t`), so `total-recall mesh ssh <node> 'claude setup-token'` can prompt. Piped or scripted, `ssh` runs under the runner timeout like any other command; use `exec` for scripted work.
37
+
34
38
  ## Owned Raw Commands
35
39
 
36
40
  This plugin owns: `ssh\s`, `scp\s`, `tailscale\s+(status|ping)\b`.
@@ -402,8 +402,11 @@ export default async function meshCli(argv = []) {
402
402
  return;
403
403
  }
404
404
 
405
+ // Run from a terminal, total-recall attaches this command to it (manifest
406
+ // `interactive`), so ssh reads and writes the person's terminal directly.
405
407
  const sshArgs = buildSshArgs(resolved, {
406
408
  command: args.length ? args.join(' ') : null,
409
+ terminal: process.stdin.isTTY === true && process.stdout.isTTY === true,
407
410
  });
408
411
  const res = spawnSync('ssh', sshArgs, { stdio: 'inherit' });
409
412
  process.exitCode = res.status ?? 1;
@@ -234,10 +234,13 @@ export function formatAccessTarget(resolved) {
234
234
  * cannot silently offer a different key first and get the connection refused
235
235
  * before the right key is ever tried.
236
236
  */
237
- export function buildSshArgs(resolved, { command = null, extraOptions = [] } = {}) {
237
+ export function buildSshArgs(resolved, { command = null, extraOptions = [], terminal = false } = {}) {
238
238
  if (!resolved?.complete) return null;
239
239
 
240
240
  const args = [];
241
+ // ssh gives a remote command no terminal unless asked, so a sign-in run from a
242
+ // person's terminal (`mesh ssh node 'claude setup-token'`) could not prompt.
243
+ if (terminal && command) args.push('-t');
241
244
  if (resolved.port && resolved.port !== DEFAULT_SSH_PORT) args.push('-p', String(resolved.port));
242
245
  if (resolved.identity_file) {
243
246
  args.push('-i', resolved.identity_file, '-o', 'IdentitiesOnly=yes');
@@ -7,6 +7,8 @@ Credential lookup, control-server API keys and mesh sync tokens use auth. Enroll
7
7
  Presence accepts fresh authenticated observations. Unknown activity cannot imply an active user. No active target is an error for automatic dispatch. Commands succeed only after their actual execution result; notification acknowledgement is not proof the user viewed it. Test timeouts, stale records, denied peers, local execution and remote failures. Never copy/merge brains during leader changes. Significant mesh extraction and truthfulness work remains in progress until owner and integration suites pass.
8
8
 
9
9
 
10
+ `mesh ssh <node>` is declared `interactive`: from a terminal it is a real session with no time limit (window resizes and Ctrl-C reach ssh). `mesh ssh <node> '<command>'` from a terminal gets a remote terminal (`ssh -t`), so sign-ins such as `claude setup-token` work. Piped or scripted, it runs as before under the runner timeout; use `mesh exec` for scripted commands.
11
+
10
12
  Host service adapters require the explicitly selected absolute TR_PACKAGE_ROOT; independently installed host packages are never execution substitutes. The mesh owner runtime suite passes 249 tests plus three Node manifest/host-boundary tests; these do not prove live mesh provider health.
11
13
 
12
14
  ## Developer-owned control server
@@ -8,6 +8,8 @@ Plugin UI is purpose-specific and belongs in its repository. Bind cards to actua
8
8
 
9
9
  The bridge rejects undeclared commands even when a manifest declares no commands. Cards execute only explicitly declared read effects. Owner panels may submit one declared secret string with `host.run(command, args, { stdin: value })`; the authenticated server validates the declaration and 64 KiB UTF-8 bound, pipes it with `--stdin`, and suppresses child output. Workspace components resolve to owner UI element ids/tags; missing interfaces and unsupported device access stay unavailable. Live card regions mount owner panels. Full provider behavior and audio sessions still require separate end-to-end verification.
10
10
 
11
+ Plugin commands run in a child process with piped, bounded output and a timeout (`timeout_ms`, 1..600000; default 30 s). A subcommand that is a terminal session, such as a remote shell or a sign-in, declares `interactive: true`. When the terminal CLI runs it with stdin and stdout on a TTY, the child is attached to the terminal (all three streams, the terminal's foreground process group) with no timeout, and the CLI exits with its code. The server, scripts, piped calls and composable calls keep piped output and the timeout.
12
+
11
13
  Configuration uses validated records/SSSS contracts. A manifest task schedule is not an active runner. Resolve node placement and daemon evidence; do not run recurring work on an arbitrary laptop. Verify tests/package provenance separately from install/runtime readiness. Keep the owner skill and this router's relevant reference synchronized whenever capabilities change.
12
14
 
13
15
 
@@ -53,6 +53,8 @@ export function validateManifestContract(manifest) {
53
53
  // The runner refuses anything outside this range, so a manifest that
54
54
  // declares it must fail validation rather than every call at run time.
55
55
  if (sub.timeout_ms !== undefined && (!Number.isInteger(sub.timeout_ms) || sub.timeout_ms < 1 || sub.timeout_ms > 600000)) errors.push(`${where}.timeout_ms must be an integer from 1 to 600000`);
56
+ // Run from a terminal, an interactive command is attached to it with no timeout (plugin-runner.mjs).
57
+ if (sub.interactive !== undefined && typeof sub.interactive !== 'boolean') errors.push(`${where}.interactive must be a boolean`);
56
58
  if (sub.args !== undefined) {
57
59
  if (!Array.isArray(sub.args)) errors.push(`${where}.args must be an array`);
58
60
  else {
@@ -24,6 +24,11 @@ export const CLI_MAX_OUTPUT_BYTES = 16 * 1024 * 1024;
24
24
  export const MAX_INPUT_BYTES = 64 * 1024;
25
25
  export const MAX_REQUEST_BYTES = 1024 * 1024;
26
26
 
27
+ /** Both ends a person types into and reads from are a terminal. */
28
+ export function stdioIsTerminal() {
29
+ return process.stdin.isTTY === true && process.stdout.isTTY === true;
30
+ }
31
+
27
32
  /** A credential is one declared string on stdin, never a command-line value. */
28
33
  export function validatePluginSecretInput(plugin, subcommand, input) {
29
34
  if (input === undefined) return;
@@ -39,8 +44,9 @@ export function validatePluginSecretInput(plugin, subcommand, input) {
39
44
 
40
45
  /**
41
46
  * @param {object} plugin A discovered plugin ({ id, dir, manifest }).
42
- * @param {{ subcommand?: string, args?: string[], cwd?: string, projectRoot?: string|null, timeoutMs?: number }} [options]
43
- * @returns {Promise<{ ok: boolean, exitCode: number|null, signal: string|null, timedOut: boolean, truncated: boolean, output: string, durationMs: number }>}
47
+ * @param {{ subcommand?: string, args?: string[], cwd?: string, projectRoot?: string|null, timeoutMs?: number, tty?: boolean }} [options]
48
+ * `tty` overrides terminal detection (stdin and stdout are TTYs) for an interactive subcommand.
49
+ * @returns {Promise<{ ok: boolean, exitCode: number|null, signal: string|null, timedOut: boolean, truncated: boolean, output: string, durationMs: number, interactive?: boolean }>}
44
50
  */
45
51
  export function runPluginCommand(plugin, options = {}) {
46
52
  const { subcommand = '', args = [], cwd = process.cwd(), timeoutMs: requestedTimeoutMs, stdin = 'ignore', stdinText, stdinRequest, composable = false, secretsBrainDir } = options;
@@ -95,6 +101,24 @@ export function runPluginCommand(plugin, options = {}) {
95
101
  const childArgv = [CHILD, handler, command, ...(subcommand ? [String(subcommand)] : []), ...safeArgs];
96
102
  // Only an explicitly selected project owns repository state. The cwd fallback above preserves legacy activation.
97
103
  const statePluginsDir = options.projectRoot ? projectPluginsDir(projectRoot) : recordPluginsDir(plugin);
104
+ const env = {
105
+ ...process.env,
106
+ TR_PACKAGE_ROOT: PACKAGE_ROOT,
107
+ TOTAL_RECALL_BIN: path.join(PACKAGE_ROOT, 'bin', 'total-recall.mjs'),
108
+ TR_PLUGIN_ID: plugin.id,
109
+ TR_PLUGIN_DIR: plugin.dir,
110
+ TR_PLUGIN_CONFIG: JSON.stringify(readPluginRecord(plugin)?.configuration || {}),
111
+ ...(statePluginsDir ? { TR_PLUGIN_STATE_DIR: path.join(path.dirname(statePluginsDir), 'plugin-state', plugin.id) } : {}),
112
+ ...(secretsBrainDir ? { TR_SECRETS_BRAIN: secretsBrainDir } : {}),
113
+ TR_PLUGIN_COMMAND_MODE: composable ? 'composable' : 'cli',
114
+ };
115
+
116
+ // An interactive subcommand (a remote shell, a sign-in) run by a person at a terminal is
117
+ // attached to that terminal: no pipes, no output buffer, no timer. Every other caller (the
118
+ // server, a script, a nested composable call, a stdin request) keeps the bounded path below.
119
+ const attach = declaration?.interactive === true && stdin === 'inherit' && stdinPayload === undefined &&
120
+ !composable && (options.tty ?? stdioIsTerminal());
121
+ if (attach) return runAttached(childArgv, { cwd, env });
98
122
 
99
123
  return new Promise((resolve) => {
100
124
  const started = Date.now();
@@ -108,19 +132,7 @@ export function runPluginCommand(plugin, options = {}) {
108
132
  const child = spawn(process.execPath, childArgv, {
109
133
  cwd,
110
134
  detached: process.platform !== 'win32',
111
- env: {
112
- ...process.env,
113
- TR_PACKAGE_ROOT: PACKAGE_ROOT,
114
- TOTAL_RECALL_BIN: path.join(PACKAGE_ROOT, 'bin', 'total-recall.mjs'),
115
- TR_PLUGIN_ID: plugin.id,
116
- TR_PLUGIN_DIR: plugin.dir,
117
- TR_PLUGIN_CONFIG: JSON.stringify(readPluginRecord(plugin)?.configuration || {}),
118
- ...(statePluginsDir ? { TR_PLUGIN_STATE_DIR: path.join(path.dirname(statePluginsDir), 'plugin-state', plugin.id) } : {}),
119
- ...(secretsBrainDir ? { TR_SECRETS_BRAIN: secretsBrainDir } : {}),
120
- TR_PLUGIN_COMMAND_MODE: composable ? 'composable' : 'cli',
121
- FORCE_COLOR: '0',
122
- NO_COLOR: '1'
123
- },
135
+ env: { ...env, FORCE_COLOR: '0', NO_COLOR: '1' },
124
136
  stdio: [stdinPayload !== undefined ? 'pipe' : stdin === 'inherit' ? 'inherit' : 'ignore', 'pipe', 'pipe']
125
137
  });
126
138
 
@@ -181,3 +193,32 @@ export function runPluginCommand(plugin, options = {}) {
181
193
  child.on('close', (code, signal) => finish(code, signal));
182
194
  });
183
195
  }
196
+
197
+ /**
198
+ * Run a handler on this process's own terminal. The child is not detached: it stays in the
199
+ * terminal's session and foreground process group, so it gets window resizes (SIGWINCH), Ctrl-C
200
+ * and the other terminal signals itself. Like a shell waiting on a job, this process ignores
201
+ * SIGINT and SIGQUIT until the child exits, then reports the child's exit code.
202
+ */
203
+ function runAttached(childArgv, { cwd, env }) {
204
+ return new Promise((resolve) => {
205
+ const started = Date.now();
206
+ const signals = process.platform === 'win32' ? ['SIGINT'] : ['SIGINT', 'SIGQUIT'];
207
+ const ignore = () => {};
208
+ for (const name of signals) process.on(name, ignore);
209
+ let settled = false;
210
+ const finish = (exitCode, signal, spawnError) => {
211
+ if (settled) return;
212
+ settled = true;
213
+ for (const name of signals) process.removeListener(name, ignore);
214
+ const message = spawnError ? `[runner] ${spawnError.message}` : '';
215
+ resolve({
216
+ ok: !spawnError && exitCode === 0, exitCode, signal, timedOut: false, truncated: false, interactive: true,
217
+ output: message, stdout: '', stderr: message, durationMs: Date.now() - started,
218
+ });
219
+ };
220
+ const child = spawn(process.execPath, childArgv, { cwd, env, stdio: 'inherit' });
221
+ child.on('error', (err) => finish(null, null, err));
222
+ child.on('close', (code, signal) => finish(code, signal));
223
+ });
224
+ }