@demicodes/host-local 0.10.1 → 0.10.3

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/dist/index.d.mts CHANGED
@@ -1,14 +1,14 @@
1
1
  import { AgentHarness, AgentServer, AgentServerSessionOptions } from "@demicodes/agent";
2
- import { resolveDemiHome } from "@demicodes/provider/credentials-pool";
3
2
  import { BashEnvironmentOptions, Host, HostFileSystem, HostProcess, HostStore } from "@demicodes/shell";
4
3
  import { Provider } from "@demicodes/provider";
4
+
5
5
  //#region src/command-bridge.d.ts
6
6
  /**
7
7
  * Dispatch script shared by every generated command-name symlink.
8
8
  * Reads the invoked name from the symlink path, applies the stdin grace
9
9
  * contract from docs/command-bridge.md, and POSTs /run on the UDS socket.
10
10
  */
11
- declare const COMMAND_BRIDGE_SHIM_SOURCE = "#!/usr/bin/env node\nconst { request } = require('node:http')\nconst { basename } = require('node:path')\n\n// Stdin policy (docs/command-bridge.md): if no byte arrives within the grace\n// window, proceed with empty stdin. Once any byte arrives, clear the timer\n// and read until EOF with no time cap. Late data after empty-stdin dispatch\n// is reported on stderr and not pretended to have been delivered.\nconst STDIN_GRACE_MS = 300\n\nfunction readStdin() {\n if (process.stdin.isTTY) return Promise.resolve('')\n return new Promise((resolve, reject) => {\n const chunks = []\n let resolved = false\n let timedOut = false\n let timer\n const cleanup = () => {\n clearTimeout(timer)\n process.stdin.removeListener('data', onData)\n process.stdin.removeListener('end', onEnd)\n process.stdin.removeListener('error', onError)\n }\n const onData = (chunk) => {\n if (timedOut) {\n process.stderr.write(\n 'command bridge: ' + chunk.length + ' byte(s) of stdin arrived after the ' + STDIN_GRACE_MS +\n 'ms grace period elapsed; the command already ran with empty stdin\\n',\n )\n cleanup()\n return\n }\n clearTimeout(timer)\n chunks.push(chunk)\n }\n const onEnd = () => {\n if (timedOut) {\n cleanup()\n return\n }\n resolved = true\n cleanup()\n resolve(Buffer.concat(chunks).toString('utf8'))\n }\n const onError = (error) => {\n if (resolved || timedOut) return\n resolved = true\n cleanup()\n reject(error)\n }\n timer = setTimeout(() => {\n timedOut = true\n resolved = true\n resolve('')\n }, STDIN_GRACE_MS)\n process.stdin.on('data', onData)\n process.stdin.on('end', onEnd)\n process.stdin.on('error', onError)\n })\n}\n\nfunction postRun(socketPath, body) {\n return new Promise((resolve, reject) => {\n const req = request(\n { socketPath, path: '/run', method: 'POST', headers: { 'content-type': 'application/json' } },\n (res) => {\n const chunks = []\n res.on('data', (chunk) => chunks.push(chunk))\n res.on('end', () => resolve({ statusCode: res.statusCode || 0, body: Buffer.concat(chunks).toString('utf8') }))\n },\n )\n req.on('error', reject)\n req.end(body)\n })\n}\n\nasync function main() {\n const socketPath = process.env.DEMI_COMMAND_BRIDGE_SOCK\n const shellId = process.env.DEMI_SHELL_ID\n if (!socketPath || !shellId) {\n process.stderr.write('command bridge: DEMI_COMMAND_BRIDGE_SOCK / shell id not set in this shell\\n')\n process.exit(1)\n }\n const name = basename(process.argv[1] || '')\n const args = process.argv.slice(2)\n const stdin = await readStdin()\n const body = JSON.stringify({ shellId, name, args, cwd: process.cwd(), stdin })\n\n const response = await postRun(socketPath, body)\n if (response.statusCode !== 200) {\n let message = response.body\n try {\n message = JSON.parse(response.body).error || message\n } catch {}\n process.stderr.write('command bridge: ' + message + '\\n')\n process.exit(1)\n }\n const result = JSON.parse(response.body)\n if (result.stdout) {\n // Binary final streams arrive base64-encoded; write raw bytes so external\n // pipes (demi read a.png | ffmpeg -i - ...) stay byte-clean.\n process.stdout.write(result.stdoutEncoding === 'base64' ? Buffer.from(result.stdout, 'base64') : result.stdout)\n }\n if (result.stderr) process.stderr.write(result.stderr)\n process.exit(result.exitCode)\n}\n\nmain().catch((error) => {\n process.stderr.write('command bridge: ' + (error && error.message ? error.message : String(error)) + '\\n')\n process.exit(1)\n})\n";
11
+ declare const COMMAND_BRIDGE_SHIM_SOURCE = "#!/usr/bin/env node\nconst { request } = require('node:http')\nconst { basename } = require('node:path')\n\n// Stdin policy (docs/command-bridge.md): if no byte arrives within the grace\n// window, proceed with empty stdin. Once any byte arrives, clear the timer\n// and read until EOF with no time cap. Late data after empty-stdin dispatch\n// is reported on stderr and not pretended to have been delivered.\nconst STDIN_GRACE_MS = 300\n\nfunction readStdin() {\n if (process.stdin.isTTY) return Promise.resolve('')\n return new Promise((resolve, reject) => {\n const chunks = []\n let resolved = false\n let timedOut = false\n let timer\n const cleanup = () => {\n clearTimeout(timer)\n process.stdin.removeListener('data', onData)\n process.stdin.removeListener('end', onEnd)\n process.stdin.removeListener('error', onError)\n }\n const onData = (chunk) => {\n if (timedOut) {\n process.stderr.write(\n 'command bridge: ' + chunk.length + ' byte(s) of stdin arrived after the ' + STDIN_GRACE_MS +\n 'ms grace period elapsed; the command already ran with empty stdin\\n',\n )\n cleanup()\n return\n }\n clearTimeout(timer)\n chunks.push(chunk)\n }\n const onEnd = () => {\n if (timedOut) {\n cleanup()\n return\n }\n resolved = true\n cleanup()\n resolve(Buffer.concat(chunks).toString('utf8'))\n }\n const onError = (error) => {\n if (resolved || timedOut) return\n resolved = true\n cleanup()\n reject(error)\n }\n timer = setTimeout(() => {\n timedOut = true\n resolved = true\n resolve('')\n }, STDIN_GRACE_MS)\n process.stdin.on('data', onData)\n process.stdin.on('end', onEnd)\n process.stdin.on('error', onError)\n })\n}\n\nfunction postRun(socketPath, body) {\n return new Promise((resolve, reject) => {\n const req = request(\n { socketPath, path: '/run', method: 'POST', headers: { 'content-type': 'application/json' } },\n (res) => {\n const chunks = []\n res.on('data', (chunk) => chunks.push(chunk))\n res.on('end', () => resolve({ statusCode: res.statusCode || 0, body: Buffer.concat(chunks).toString('utf8') }))\n },\n )\n req.on('error', reject)\n req.end(body)\n })\n}\n\nasync function main() {\n const socketPath = process.env.DEMI_COMMAND_BRIDGE_SOCK\n // Shell sessions export DEMI_SESSION_ID as the agent session id (command scope).\n const commandScopeId = process.env.DEMI_AGENT_SESSION_ID || process.env.DEMI_SESSION_ID\n if (!socketPath || !commandScopeId) {\n process.stderr.write('command bridge: DEMI_COMMAND_BRIDGE_SOCK / session id not set in this shell\\n')\n process.exit(1)\n }\n const name = basename(process.argv[1] || '')\n const args = process.argv.slice(2)\n const stdin = await readStdin()\n const body = JSON.stringify({ commandScopeId, name, args, cwd: process.cwd(), stdin })\n\n const response = await postRun(socketPath, body)\n if (response.statusCode !== 200) {\n let message = response.body\n try {\n message = JSON.parse(response.body).error || message\n } catch {}\n process.stderr.write('command bridge: ' + message + '\\n')\n process.exit(1)\n }\n const result = JSON.parse(response.body)\n if (result.stdout) process.stdout.write(result.stdout)\n if (result.stderr) process.stderr.write(result.stderr)\n process.exit(result.exitCode)\n}\n\nmain().catch((error) => {\n process.stderr.write('command bridge: ' + (error && error.message ? error.message : String(error)) + '\\n')\n process.exit(1)\n})\n";
12
12
  interface CommandBridgeOptions {
13
13
  socketPath: string;
14
14
  }
@@ -42,6 +42,11 @@ declare function bridgeBinDirFor(stateDir: string): string;
42
42
  declare function materializeCommandBridgeShims(options: MaterializeCommandBridgeShimsOptions): Promise<string>;
43
43
  //#endregion
44
44
  //#region src/demi-home.d.ts
45
+ /**
46
+ * Demi local state root (not the workspace cwd).
47
+ * Override with `DEMI_HOME` or `createLocalAgentServer({ stateDir })`, else `~/.demi`.
48
+ */
49
+ declare function resolveDemiHome(explicit?: string): string;
45
50
  /** Fixed layout: `<stateDir>/bridges/<id>.sock` (short id for macOS AF_UNIX limits). */
46
51
  declare function defaultBridgeSocketPath(stateDir: string, serverId?: string): string;
47
52
  //#endregion
@@ -92,7 +97,7 @@ interface LocalAgentServerHandle {
92
97
  * bridge **on by default**.
93
98
  *
94
99
  * Owns the Node-only bridge transport (UDS + PATH shims under `stateDir`).
95
- * AgentServer only receives a generic `prepareShell` hook and exposes
100
+ * AgentServer only receives a generic `prepareSessionShell` hook and exposes
96
101
  * `runCommandLine` — it never sees bin dirs or sockets.
97
102
  *
98
103
  * Default layout under `stateDir` (`~/.demi` or `$DEMI_HOME`):
package/dist/index.mjs CHANGED
@@ -1,12 +1,11 @@
1
1
  import { existsSync, mkdirSync, unlinkSync } from "node:fs";
2
2
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
3
3
  import { createServer } from "node:http";
4
- import { AgentServer, RunCommandLineCommandNotRegisteredError, RunCommandLineShellNotFoundError, RunCommandLineTimeoutError } from "@demicodes/agent";
4
+ import { AgentServer, RunCommandLineSessionNotFoundError, RunCommandLineTimeoutError } from "@demicodes/agent";
5
5
  import { encodeUtf8, errorMessage, isFileNotFoundError, parsePortableJson, stringifyPortableJson } from "@demicodes/utils";
6
6
  import { createHash, randomUUID } from "node:crypto";
7
- import { resolveDemiHome } from "@demicodes/provider/credentials-pool";
8
- import { spawn } from "node:child_process";
9
7
  import { homedir } from "node:os";
8
+ import { spawn } from "node:child_process";
10
9
  import { appendFile, chmod, cp, link, lstat, mkdir, readFile, readdir, readlink, realpath, rename, rm, stat, symlink, utimes, writeFile } from "node:fs/promises";
11
10
  //#region src/command-bridge.ts
12
11
  /**
@@ -92,15 +91,16 @@ function postRun(socketPath, body) {
92
91
 
93
92
  async function main() {
94
93
  const socketPath = process.env.DEMI_COMMAND_BRIDGE_SOCK
95
- const shellId = process.env.DEMI_SHELL_ID
96
- if (!socketPath || !shellId) {
97
- process.stderr.write('command bridge: DEMI_COMMAND_BRIDGE_SOCK / shell id not set in this shell\\n')
94
+ // Shell sessions export DEMI_SESSION_ID as the agent session id (command scope).
95
+ const commandScopeId = process.env.DEMI_AGENT_SESSION_ID || process.env.DEMI_SESSION_ID
96
+ if (!socketPath || !commandScopeId) {
97
+ process.stderr.write('command bridge: DEMI_COMMAND_BRIDGE_SOCK / session id not set in this shell\\n')
98
98
  process.exit(1)
99
99
  }
100
100
  const name = basename(process.argv[1] || '')
101
101
  const args = process.argv.slice(2)
102
102
  const stdin = await readStdin()
103
- const body = JSON.stringify({ shellId, name, args, cwd: process.cwd(), stdin })
103
+ const body = JSON.stringify({ commandScopeId, name, args, cwd: process.cwd(), stdin })
104
104
 
105
105
  const response = await postRun(socketPath, body)
106
106
  if (response.statusCode !== 200) {
@@ -112,11 +112,7 @@ async function main() {
112
112
  process.exit(1)
113
113
  }
114
114
  const result = JSON.parse(response.body)
115
- if (result.stdout) {
116
- // Binary final streams arrive base64-encoded; write raw bytes so external
117
- // pipes (demi read a.png | ffmpeg -i - ...) stay byte-clean.
118
- process.stdout.write(result.stdoutEncoding === 'base64' ? Buffer.from(result.stdout, 'base64') : result.stdout)
119
- }
115
+ if (result.stdout) process.stdout.write(result.stdout)
120
116
  if (result.stderr) process.stderr.write(result.stderr)
121
117
  process.exit(result.exitCode)
122
118
  }
@@ -152,7 +148,7 @@ async function handleRun(server, req, res) {
152
148
  return;
153
149
  }
154
150
  if (!isRunRequestBody(body)) {
155
- sendJson(res, 400, { error: "bad request: expected { shellId, name, args, cwd, stdin }" });
151
+ sendJson(res, 400, { error: "bad request: expected { commandScopeId, name, args, cwd, stdin }" });
156
152
  return;
157
153
  }
158
154
  const controller = new AbortController();
@@ -162,7 +158,7 @@ async function handleRun(server, req, res) {
162
158
  };
163
159
  req.on("close", onClosedEarly);
164
160
  try {
165
- const result = await server.runCommandLine(body.shellId, body.name, body.args, {
161
+ const result = await server.runCommandLine(body.commandScopeId, body.name, body.args, {
166
162
  cwd: body.cwd,
167
163
  stdin: body.stdin,
168
164
  signal: controller.signal
@@ -177,15 +173,14 @@ async function handleRun(server, req, res) {
177
173
  }
178
174
  }
179
175
  function statusForError(error) {
180
- if (error instanceof RunCommandLineShellNotFoundError) return 404;
181
- if (error instanceof RunCommandLineCommandNotRegisteredError) return 404;
176
+ if (error instanceof RunCommandLineSessionNotFoundError) return 404;
182
177
  if (error instanceof RunCommandLineTimeoutError) return 504;
183
178
  return 500;
184
179
  }
185
180
  function isRunRequestBody(value) {
186
181
  if (value === null || typeof value !== "object") return false;
187
182
  const record = value;
188
- return typeof record.shellId === "string" && typeof record.name === "string" && Array.isArray(record.args) && record.args.every((arg) => typeof arg === "string") && typeof record.cwd === "string" && typeof record.stdin === "string";
183
+ return typeof record.commandScopeId === "string" && typeof record.name === "string" && Array.isArray(record.args) && record.args.every((arg) => typeof arg === "string") && typeof record.cwd === "string" && typeof record.stdin === "string";
189
184
  }
190
185
  function readJsonBody(req) {
191
186
  return new Promise((resolve, reject) => {
@@ -227,7 +222,7 @@ const DISPATCH_PACKAGE_JSON = "{\"type\":\"commonjs\"}\n";
227
222
  * component under Host.fs, so it is validated here rather than trusted.
228
223
  */
229
224
  function assertPathSafeSessionId(agentSessionId) {
230
- if (agentSessionId.length === 0 || agentSessionId.includes("\0") || agentSessionId.includes("/") || agentSessionId.includes("\\") || agentSessionId === "." || agentSessionId === "..") throw new Error(`Command bridge: agentSessionId "${agentSessionId}" is not safe to use as a path segment`);
225
+ if (agentSessionId.length === 0 || agentSessionId.includes("/") || agentSessionId.includes("\\") || agentSessionId === "..") throw new Error(`Command bridge: agentSessionId "${agentSessionId}" is not safe to use as a path segment`);
231
226
  }
232
227
  /** Fixed relative layout under stateDir. */
233
228
  function bridgeBinDirFor(stateDir) {
@@ -261,6 +256,16 @@ async function materializeCommandBridgeShims(options) {
261
256
  }
262
257
  //#endregion
263
258
  //#region src/demi-home.ts
259
+ /**
260
+ * Demi local state root (not the workspace cwd).
261
+ * Override with `DEMI_HOME` or `createLocalAgentServer({ stateDir })`, else `~/.demi`.
262
+ */
263
+ function resolveDemiHome(explicit) {
264
+ if (explicit && explicit.trim()) return resolve(explicit.trim());
265
+ const fromEnv = process.env.DEMI_HOME;
266
+ if (fromEnv && fromEnv.trim()) return resolve(fromEnv.trim());
267
+ return join(homedir(), ".demi");
268
+ }
264
269
  /** Fixed layout: `<stateDir>/bridges/<id>.sock` (short id for macOS AF_UNIX limits). */
265
270
  function defaultBridgeSocketPath(stateDir, serverId = randomUUID().replace(/-/g, "").slice(0, 12)) {
266
271
  return join(stateDir, "bridges", `${serverId}.sock`);
@@ -496,16 +501,16 @@ var LocalHostFileSystem = class {
496
501
  return readdir(target);
497
502
  }
498
503
  async mkdir(path, options) {
499
- await mkdir(this.resolvePath(path, options?.cwd), { recursive: options?.recursive === true });
504
+ await mkdir(this.resolvePath(path, options?.cwd), { recursive: options?.recursive });
500
505
  }
501
506
  async rm(path, options) {
502
507
  await rm(this.resolvePath(path, options?.cwd), {
503
- recursive: options?.recursive === true,
504
- force: options?.force === true
508
+ recursive: options?.recursive,
509
+ force: options?.force
505
510
  });
506
511
  }
507
512
  async cp(path, destination, options) {
508
- await cp(this.resolvePath(path, options?.cwd), this.resolvePath(destination, options?.cwd), { recursive: options?.recursive === true });
513
+ await cp(this.resolvePath(path, options?.cwd), this.resolvePath(destination, options?.cwd), { recursive: options?.recursive });
509
514
  }
510
515
  async mv(path, destination, options) {
511
516
  await rename(this.resolvePath(path, options?.cwd), this.resolvePath(destination, options?.cwd));
@@ -577,7 +582,7 @@ async function* streamBytes(stream) {
577
582
  * bridge **on by default**.
578
583
  *
579
584
  * Owns the Node-only bridge transport (UDS + PATH shims under `stateDir`).
580
- * AgentServer only receives a generic `prepareShell` hook and exposes
585
+ * AgentServer only receives a generic `prepareSessionShell` hook and exposes
581
586
  * `runCommandLine` — it never sees bin dirs or sockets.
582
587
  *
583
588
  * Default layout under `stateDir` (`~/.demi` or `$DEMI_HOME`):
@@ -610,7 +615,7 @@ function createLocalAgentServer(options) {
610
615
  if (bridgeEnabled && socketPath && stateDir) {
611
616
  const bridgeStateDir = stateDir;
612
617
  const bridgeSocketPath = socketPath;
613
- serverOptions.prepareShell = async ({ host, agentSessionId, commandNames, shell }) => {
618
+ serverOptions.prepareSessionShell = async ({ host, agentSessionId, commandNames, shell }) => {
614
619
  const shimDir = await materializeCommandBridgeShims({
615
620
  host,
616
621
  agentSessionId,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@demicodes/host-local",
3
3
  "description": "Node LocalHost and open-box local AgentServer assembly (command bridge on by default).",
4
- "version": "0.10.1",
4
+ "version": "0.10.3",
5
5
  "private": false,
6
6
  "type": "module",
7
7
  "exports": {
@@ -11,10 +11,10 @@
11
11
  }
12
12
  },
13
13
  "dependencies": {
14
- "@demicodes/agent": "^0.10.1",
15
- "@demicodes/provider": "^0.10.1",
16
- "@demicodes/shell": "^0.10.1",
17
- "@demicodes/utils": "^0.10.1"
14
+ "@demicodes/agent": "^0.10.3",
15
+ "@demicodes/provider": "^0.10.3",
16
+ "@demicodes/shell": "^0.10.3",
17
+ "@demicodes/utils": "^0.10.3"
18
18
  },
19
19
  "license": "Apache-2.0",
20
20
  "main": "./dist/index.mjs",