@demicodes/host-local 0.10.3 → 0.11.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.
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";
2
3
  import { BashEnvironmentOptions, Host, HostFileSystem, HostProcess, HostStore } from "@demicodes/shell";
3
4
  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 // 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";
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";
12
12
  interface CommandBridgeOptions {
13
13
  socketPath: string;
14
14
  }
@@ -42,11 +42,6 @@ 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;
50
45
  /** Fixed layout: `<stateDir>/bridges/<id>.sock` (short id for macOS AF_UNIX limits). */
51
46
  declare function defaultBridgeSocketPath(stateDir: string, serverId?: string): string;
52
47
  //#endregion
@@ -97,7 +92,7 @@ interface LocalAgentServerHandle {
97
92
  * bridge **on by default**.
98
93
  *
99
94
  * Owns the Node-only bridge transport (UDS + PATH shims under `stateDir`).
100
- * AgentServer only receives a generic `prepareSessionShell` hook and exposes
95
+ * AgentServer only receives a generic `prepareShell` hook and exposes
101
96
  * `runCommandLine` — it never sees bin dirs or sockets.
102
97
  *
103
98
  * Default layout under `stateDir` (`~/.demi` or `$DEMI_HOME`):
package/dist/index.mjs CHANGED
@@ -1,11 +1,12 @@
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, RunCommandLineSessionNotFoundError, RunCommandLineTimeoutError } from "@demicodes/agent";
4
+ import { AgentServer, RunCommandLineCommandNotRegisteredError, RunCommandLineShellNotFoundError, 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 { homedir } from "node:os";
7
+ import { resolveDemiHome } from "@demicodes/provider/credentials-pool";
8
8
  import { spawn } from "node:child_process";
9
+ import { homedir } from "node:os";
9
10
  import { appendFile, chmod, cp, link, lstat, mkdir, readFile, readdir, readlink, realpath, rename, rm, stat, symlink, utimes, writeFile } from "node:fs/promises";
10
11
  //#region src/command-bridge.ts
11
12
  /**
@@ -91,16 +92,15 @@ function postRun(socketPath, body) {
91
92
 
92
93
  async function main() {
93
94
  const socketPath = process.env.DEMI_COMMAND_BRIDGE_SOCK
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')
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')
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({ commandScopeId, name, args, cwd: process.cwd(), stdin })
103
+ const body = JSON.stringify({ shellId, name, args, cwd: process.cwd(), stdin })
104
104
 
105
105
  const response = await postRun(socketPath, body)
106
106
  if (response.statusCode !== 200) {
@@ -112,7 +112,11 @@ async function main() {
112
112
  process.exit(1)
113
113
  }
114
114
  const result = JSON.parse(response.body)
115
- if (result.stdout) process.stdout.write(result.stdout)
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
+ }
116
120
  if (result.stderr) process.stderr.write(result.stderr)
117
121
  process.exit(result.exitCode)
118
122
  }
@@ -148,7 +152,7 @@ async function handleRun(server, req, res) {
148
152
  return;
149
153
  }
150
154
  if (!isRunRequestBody(body)) {
151
- sendJson(res, 400, { error: "bad request: expected { commandScopeId, name, args, cwd, stdin }" });
155
+ sendJson(res, 400, { error: "bad request: expected { shellId, name, args, cwd, stdin }" });
152
156
  return;
153
157
  }
154
158
  const controller = new AbortController();
@@ -158,7 +162,7 @@ async function handleRun(server, req, res) {
158
162
  };
159
163
  req.on("close", onClosedEarly);
160
164
  try {
161
- const result = await server.runCommandLine(body.commandScopeId, body.name, body.args, {
165
+ const result = await server.runCommandLine(body.shellId, body.name, body.args, {
162
166
  cwd: body.cwd,
163
167
  stdin: body.stdin,
164
168
  signal: controller.signal
@@ -173,14 +177,15 @@ async function handleRun(server, req, res) {
173
177
  }
174
178
  }
175
179
  function statusForError(error) {
176
- if (error instanceof RunCommandLineSessionNotFoundError) return 404;
180
+ if (error instanceof RunCommandLineShellNotFoundError) return 404;
181
+ if (error instanceof RunCommandLineCommandNotRegisteredError) return 404;
177
182
  if (error instanceof RunCommandLineTimeoutError) return 504;
178
183
  return 500;
179
184
  }
180
185
  function isRunRequestBody(value) {
181
186
  if (value === null || typeof value !== "object") return false;
182
187
  const record = value;
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";
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";
184
189
  }
185
190
  function readJsonBody(req) {
186
191
  return new Promise((resolve, reject) => {
@@ -222,7 +227,7 @@ const DISPATCH_PACKAGE_JSON = "{\"type\":\"commonjs\"}\n";
222
227
  * component under Host.fs, so it is validated here rather than trusted.
223
228
  */
224
229
  function assertPathSafeSessionId(agentSessionId) {
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`);
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`);
226
231
  }
227
232
  /** Fixed relative layout under stateDir. */
228
233
  function bridgeBinDirFor(stateDir) {
@@ -256,16 +261,6 @@ async function materializeCommandBridgeShims(options) {
256
261
  }
257
262
  //#endregion
258
263
  //#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
- }
269
264
  /** Fixed layout: `<stateDir>/bridges/<id>.sock` (short id for macOS AF_UNIX limits). */
270
265
  function defaultBridgeSocketPath(stateDir, serverId = randomUUID().replace(/-/g, "").slice(0, 12)) {
271
266
  return join(stateDir, "bridges", `${serverId}.sock`);
@@ -279,7 +274,8 @@ var LocalHostStore = class {
279
274
  }
280
275
  async readJson(key) {
281
276
  try {
282
- return parsePortableJson(await readFile(this.pathForKey(key), "utf8"));
277
+ const content = await readFile(this.pathForKey(key), "utf8");
278
+ return parsePortableJson(content);
283
279
  } catch (error) {
284
280
  if (isFileNotFoundError(error)) return null;
285
281
  throw error;
@@ -501,16 +497,16 @@ var LocalHostFileSystem = class {
501
497
  return readdir(target);
502
498
  }
503
499
  async mkdir(path, options) {
504
- await mkdir(this.resolvePath(path, options?.cwd), { recursive: options?.recursive });
500
+ await mkdir(this.resolvePath(path, options?.cwd), { recursive: options?.recursive === true });
505
501
  }
506
502
  async rm(path, options) {
507
503
  await rm(this.resolvePath(path, options?.cwd), {
508
- recursive: options?.recursive,
509
- force: options?.force
504
+ recursive: options?.recursive === true,
505
+ force: options?.force === true
510
506
  });
511
507
  }
512
508
  async cp(path, destination, options) {
513
- await cp(this.resolvePath(path, options?.cwd), this.resolvePath(destination, options?.cwd), { recursive: options?.recursive });
509
+ await cp(this.resolvePath(path, options?.cwd), this.resolvePath(destination, options?.cwd), { recursive: options?.recursive === true });
514
510
  }
515
511
  async mv(path, destination, options) {
516
512
  await rename(this.resolvePath(path, options?.cwd), this.resolvePath(destination, options?.cwd));
@@ -582,7 +578,7 @@ async function* streamBytes(stream) {
582
578
  * bridge **on by default**.
583
579
  *
584
580
  * Owns the Node-only bridge transport (UDS + PATH shims under `stateDir`).
585
- * AgentServer only receives a generic `prepareSessionShell` hook and exposes
581
+ * AgentServer only receives a generic `prepareShell` hook and exposes
586
582
  * `runCommandLine` — it never sees bin dirs or sockets.
587
583
  *
588
584
  * Default layout under `stateDir` (`~/.demi` or `$DEMI_HOME`):
@@ -615,7 +611,7 @@ function createLocalAgentServer(options) {
615
611
  if (bridgeEnabled && socketPath && stateDir) {
616
612
  const bridgeStateDir = stateDir;
617
613
  const bridgeSocketPath = socketPath;
618
- serverOptions.prepareSessionShell = async ({ host, agentSessionId, commandNames, shell }) => {
614
+ serverOptions.prepareShell = async ({ host, agentSessionId, commandNames, shell }) => {
619
615
  const shimDir = await materializeCommandBridgeShims({
620
616
  host,
621
617
  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.3",
4
+ "version": "0.11.0",
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.3",
15
- "@demicodes/provider": "^0.10.3",
16
- "@demicodes/shell": "^0.10.3",
17
- "@demicodes/utils": "^0.10.3"
14
+ "@demicodes/agent": "^0.11.0",
15
+ "@demicodes/provider": "^0.11.0",
16
+ "@demicodes/shell": "^0.11.0",
17
+ "@demicodes/utils": "^0.11.0"
18
18
  },
19
19
  "license": "Apache-2.0",
20
20
  "main": "./dist/index.mjs",