@xynogen/pix-runtime 0.8.4 → 0.10.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/package.json +4 -2
- package/src/icon-catalog.ts +4 -0
- package/src/index.ts +13 -0
- package/src/lfid.ts +86 -0
- package/src/which.ts +103 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xynogen/pix-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Pix shared runtime — versioned pix.json config, atomic persistence, typed change events",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -27,8 +27,10 @@
|
|
|
27
27
|
"./collapse": "./src/collapse.ts",
|
|
28
28
|
"./icon-catalog": "./src/icon-catalog.ts",
|
|
29
29
|
"./io": "./src/io.ts",
|
|
30
|
+
"./lfid": "./src/lfid.ts",
|
|
30
31
|
"./once": "./src/once.ts",
|
|
31
|
-
"./testing": "./src/testing.ts"
|
|
32
|
+
"./testing": "./src/testing.ts",
|
|
33
|
+
"./which": "./src/which.ts"
|
|
32
34
|
},
|
|
33
35
|
"keywords": [
|
|
34
36
|
"pi",
|
package/src/icon-catalog.ts
CHANGED
|
@@ -65,6 +65,10 @@ const CATALOG = {
|
|
|
65
65
|
// presentation, ascii = tofu-free token.
|
|
66
66
|
lock: { nerd: "\u{F0341}", unicode: `\u{1F512}${VS}`, ascii: "[!]" },
|
|
67
67
|
secret: { nerd: "\u{F0306}", unicode: `\u{1F511}${VS}`, ascii: "[key]" },
|
|
68
|
+
"data.boolean": { nerd: "\u25C6", unicode: `\u25C6${VS}`, ascii: "bool" },
|
|
69
|
+
"data.int": { nerd: "#", unicode: "#", ascii: "int" },
|
|
70
|
+
"data.float": { nerd: "\u2248", unicode: `\u2248${VS}`, ascii: "float" },
|
|
71
|
+
"data.string": { nerd: "\u201C", unicode: `\u201C${VS}`, ascii: "str" },
|
|
68
72
|
settings: { nerd: "\u{F0493}", unicode: `\u2699${VS}`, ascii: "[*]" },
|
|
69
73
|
update: { nerd: "\u{F01DA}", unicode: `\u2193${VS}`, ascii: "[v]" },
|
|
70
74
|
|
package/src/index.ts
CHANGED
|
@@ -24,6 +24,14 @@ export {
|
|
|
24
24
|
withAgentBlock,
|
|
25
25
|
} from "./herdr-state.ts";
|
|
26
26
|
export { ioTimeoutMs, ioTimeoutSignal } from "./io.ts";
|
|
27
|
+
export {
|
|
28
|
+
generateLfid,
|
|
29
|
+
isLfid,
|
|
30
|
+
LFID_RE,
|
|
31
|
+
type LfidOptions,
|
|
32
|
+
parseLfid,
|
|
33
|
+
uniqueLfid,
|
|
34
|
+
} from "./lfid.ts";
|
|
27
35
|
export {
|
|
28
36
|
config,
|
|
29
37
|
createRuntime,
|
|
@@ -60,3 +68,8 @@ export {
|
|
|
60
68
|
type PrettyConfig,
|
|
61
69
|
prettySection,
|
|
62
70
|
} from "./sections/index.ts";
|
|
71
|
+
export {
|
|
72
|
+
type FindExecutableOptions,
|
|
73
|
+
findExecutable,
|
|
74
|
+
findExecutableSync,
|
|
75
|
+
} from "./which.ts";
|
package/src/lfid.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LFID — LLM-friendly IDs (`agent-happy-walrus-42`).
|
|
3
|
+
*
|
|
4
|
+
* Raw UUID fragments (`3f2a1b9c-4d5e`, `abc123`) waste parent-model tokens and
|
|
5
|
+
* invite typos when the model must type them back (steer/stop/resume). An LFID
|
|
6
|
+
* is `[prefix]-[adjective]-[noun]-[NN]`: pronounceable, copyable, and
|
|
7
|
+
* self-evidently an ID in a transcript. Two random words + two digits give
|
|
8
|
+
* 100×100×100 = 1M combinations per prefix — plenty for a live agent set, and
|
|
9
|
+
* the owning registry (e.g. pix-subagent's AgentManager) maps LFID → record.
|
|
10
|
+
*
|
|
11
|
+
* `ponytail:` 2-digit suffix caps the namespace at 1M per prefix. If live sets
|
|
12
|
+
* ever approach that, widen the suffix or add a third word here.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { randomInt } from "node:crypto";
|
|
16
|
+
|
|
17
|
+
/** `agent-<adj>-<noun>-<NN>` shape. Prefix is caller-owned (`agent`, `job`, …). */
|
|
18
|
+
export const LFID_RE = /^[a-z][a-z0-9]*-[a-z]+-[a-z]+-\d{2}$/;
|
|
19
|
+
|
|
20
|
+
// Small curated lists: common words, no ambiguous pairs, no profanity.
|
|
21
|
+
// Sized at 100 × 100 so two digits complete a 1M namespace per prefix.
|
|
22
|
+
const ADJECTIVES = (
|
|
23
|
+
"agate amber azure birch bold brave bright brisk calm cedar cinder clever cloudy coral crisp dawn drift dusk eager elm" +
|
|
24
|
+
"ember fair fern fleet flint fresh frost garnet glad golden grand green grove happy hazy heath iris ivory jade juniper" +
|
|
25
|
+
"keen kind lagoon larch light linden lively lotus lucid lunar maple meadow merry misty moss nectar nimble nimbus noble north" +
|
|
26
|
+
"ocean olive opal pearl pine plain plum proud quartz quick quiet rapid reef ridge river rocky round royal rusty sable" +
|
|
27
|
+
"sage sandy sedge sharp shiny silent silver sleek small smart smooth solid sorrel spring steady stone sunny swift tango teal"
|
|
28
|
+
).split(" ");
|
|
29
|
+
|
|
30
|
+
const NOUNS = (
|
|
31
|
+
"adder albatross anchovy badger beagle bear beaver boar bobcat cobra condor cougar coyote crane cricket deer dove drake duck eagle" +
|
|
32
|
+
"falcon ferret finch fisher fox frog gannet gazelle gecko goose gopher grouse gull hamster hare hawk heron hyena ibis impala" +
|
|
33
|
+
"indigo jackal jaguar jay kelp kestrel koala kudu lark lemur llama lynx magpie manatee marmot marten meerkat mink mole mongoose" +
|
|
34
|
+
"moose narwhal newt numbats ocelot okapi oriole osprey otter owl ox oyster panda pangolin parrot pelican penguin pigeon pika platypus" +
|
|
35
|
+
"porpoise possum puffin python quail quokka rabbit raven rhea robin sable salmon saola seal shark shrew skunk sloth snail sparrow"
|
|
36
|
+
).split(" ");
|
|
37
|
+
|
|
38
|
+
export interface LfidOptions {
|
|
39
|
+
/** Namespace prefix, e.g. `"agent"`. Defaults to `"agent"`. */
|
|
40
|
+
prefix?: string;
|
|
41
|
+
/** RNG override for tests/determinism. Must return an int in [0, max). */
|
|
42
|
+
rand?: (max: number) => number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Generate one LFID, e.g. `agent-happy-walrus-42`. */
|
|
46
|
+
export function generateLfid(opts: LfidOptions = {}): string {
|
|
47
|
+
const prefix = opts.prefix ?? "agent";
|
|
48
|
+
const rand = opts.rand ?? randomInt;
|
|
49
|
+
const pick = (xs: string[]): string => xs[rand(xs.length)] as string;
|
|
50
|
+
const n = String(rand(100)).padStart(2, "0");
|
|
51
|
+
return `${prefix}-${pick(ADJECTIVES)}-${pick(NOUNS)}-${n}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Loose check: is this string LFID-shaped (any prefix)? */
|
|
55
|
+
export function isLfid(s: string): boolean {
|
|
56
|
+
return LFID_RE.test(s);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Parse an LFID into its parts; undefined when the shape doesn't match. */
|
|
60
|
+
export function parseLfid(
|
|
61
|
+
s: string,
|
|
62
|
+
): { prefix: string; adjective: string; noun: string; num: string } | undefined {
|
|
63
|
+
const m = LFID_RE.exec(s);
|
|
64
|
+
if (!m) return undefined;
|
|
65
|
+
const [prefix, adjective, noun, num] = s.split("-");
|
|
66
|
+
return {
|
|
67
|
+
prefix: prefix as string,
|
|
68
|
+
adjective: adjective as string,
|
|
69
|
+
noun: noun as string,
|
|
70
|
+
num: num as string,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Generate an LFID not already present in `taken`. Retries a bounded number of
|
|
76
|
+
* times, then throws — a full or near-full namespace is a caller bug, not a
|
|
77
|
+
* loop-candidate. Collisions across live sets are essentially impossible long
|
|
78
|
+
* before this bound (1M combinations), so 64 tries is generous.
|
|
79
|
+
*/
|
|
80
|
+
export function uniqueLfid(taken: (id: string) => boolean, opts: LfidOptions = {}): string {
|
|
81
|
+
for (let i = 0; i < 64; i++) {
|
|
82
|
+
const id = generateLfid(opts);
|
|
83
|
+
if (!taken(id)) return id;
|
|
84
|
+
}
|
|
85
|
+
throw new Error("lfid: namespace exhausted (too many live IDs for prefix)");
|
|
86
|
+
}
|
package/src/which.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-platform executable lookup — the `which`/`where` a portable Node program
|
|
3
|
+
* lacks. Scans `PATH` (POSIX + Windows), honouring Windows `PATHEXT` so a bare
|
|
4
|
+
* name like `aria2c` resolves to `aria2c.exe`. Returns the first match, or
|
|
5
|
+
* `undefined` when nothing on `PATH` is runnable.
|
|
6
|
+
*
|
|
7
|
+
* A name that already contains a path separator is treated as a direct path and
|
|
8
|
+
* checked in place (still PATHEXT-expanded on Windows), matching `which` semantics.
|
|
9
|
+
*
|
|
10
|
+
* `ponytail:` POSIX runnability is an `X_OK` access check — it does not re-derive
|
|
11
|
+
* effective-uid permission bits. That matches how the shell picks a binary, which
|
|
12
|
+
* is the intent here. Windows treats "exists with an executable extension" as
|
|
13
|
+
* runnable (there is no X bit); this is the conventional `where` behaviour.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { accessSync, constants, statSync } from "node:fs";
|
|
17
|
+
import { access, stat } from "node:fs/promises";
|
|
18
|
+
import { delimiter, isAbsolute, join, sep } from "node:path";
|
|
19
|
+
|
|
20
|
+
const isWindows = process.platform === "win32";
|
|
21
|
+
|
|
22
|
+
/** Windows executable extensions, from `PATHEXT` with a sane fallback. */
|
|
23
|
+
function pathExtensions(env: NodeJS.ProcessEnv): string[] {
|
|
24
|
+
if (!isWindows) return [""];
|
|
25
|
+
const raw = env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD";
|
|
26
|
+
// Leading "" lets an already-suffixed name (foo.exe) match as-is.
|
|
27
|
+
return ["", ...raw.split(";").filter(Boolean)];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Directories to scan, from `PATH`. Windows also probes the current directory first. */
|
|
31
|
+
function pathDirs(env: NodeJS.ProcessEnv): string[] {
|
|
32
|
+
const raw = env.PATH ?? env.Path ?? "";
|
|
33
|
+
const dirs = raw.split(delimiter).filter(Boolean);
|
|
34
|
+
return isWindows ? [".", ...dirs] : dirs;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface FindExecutableOptions {
|
|
38
|
+
/** Environment to read PATH/PATHEXT from. Defaults to `process.env`. */
|
|
39
|
+
env?: NodeJS.ProcessEnv;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function candidatesFor(name: string, env: NodeJS.ProcessEnv): string[] {
|
|
43
|
+
const exts = pathExtensions(env);
|
|
44
|
+
const withExts = (base: string): string[] => exts.map((ext) => base + ext);
|
|
45
|
+
// A name with a separator (or absolute) is a direct path — do not scan PATH.
|
|
46
|
+
if (isAbsolute(name) || name.includes(sep) || (isWindows && name.includes("/"))) {
|
|
47
|
+
return withExts(name);
|
|
48
|
+
}
|
|
49
|
+
return pathDirs(env).flatMap((dir) => withExts(join(dir, name)));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function runnableSync(p: string): boolean {
|
|
53
|
+
try {
|
|
54
|
+
if (!statSync(p).isFile()) return false;
|
|
55
|
+
if (isWindows) return true;
|
|
56
|
+
accessSync(p, constants.X_OK);
|
|
57
|
+
return true;
|
|
58
|
+
} catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function runnable(p: string): Promise<boolean> {
|
|
64
|
+
try {
|
|
65
|
+
const info = await stat(p);
|
|
66
|
+
if (!info.isFile()) return false;
|
|
67
|
+
if (isWindows) return true;
|
|
68
|
+
await access(p, constants.X_OK);
|
|
69
|
+
return true;
|
|
70
|
+
} catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Find a runnable executable by name on PATH. Returns its path (as joined from
|
|
77
|
+
* PATH), or `undefined` when not found. Prefer {@link findExecutableSync} unless
|
|
78
|
+
* you specifically need to avoid blocking.
|
|
79
|
+
*/
|
|
80
|
+
export async function findExecutable(
|
|
81
|
+
name: string,
|
|
82
|
+
options: FindExecutableOptions = {},
|
|
83
|
+
): Promise<string | undefined> {
|
|
84
|
+
if (!name) return undefined;
|
|
85
|
+
const env = options.env ?? process.env;
|
|
86
|
+
for (const candidate of candidatesFor(name, env)) {
|
|
87
|
+
if (await runnable(candidate)) return candidate;
|
|
88
|
+
}
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Synchronous {@link findExecutable} — the common case (startup, gate checks). */
|
|
93
|
+
export function findExecutableSync(
|
|
94
|
+
name: string,
|
|
95
|
+
options: FindExecutableOptions = {},
|
|
96
|
+
): string | undefined {
|
|
97
|
+
if (!name) return undefined;
|
|
98
|
+
const env = options.env ?? process.env;
|
|
99
|
+
for (const candidate of candidatesFor(name, env)) {
|
|
100
|
+
if (runnableSync(candidate)) return candidate;
|
|
101
|
+
}
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|