@alisio/core 0.1.0-alpha.10

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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +25 -0
  3. package/dist/application.d.ts +225 -0
  4. package/dist/application.js +950 -0
  5. package/dist/config.d.ts +481 -0
  6. package/dist/config.js +592 -0
  7. package/dist/core/compaction.d.ts +136 -0
  8. package/dist/core/compaction.js +404 -0
  9. package/dist/core/contracts.d.ts +147 -0
  10. package/dist/core/contracts.js +1 -0
  11. package/dist/core/registry.d.ts +8 -0
  12. package/dist/core/registry.js +54 -0
  13. package/dist/core/runner.d.ts +160 -0
  14. package/dist/core/runner.js +625 -0
  15. package/dist/extensions/registry.d.ts +28 -0
  16. package/dist/extensions/registry.js +58 -0
  17. package/dist/index.d.ts +29 -0
  18. package/dist/index.js +28 -0
  19. package/dist/integrations/herdr.d.ts +34 -0
  20. package/dist/integrations/herdr.js +174 -0
  21. package/dist/mcp/connector.d.ts +65 -0
  22. package/dist/mcp/connector.js +423 -0
  23. package/dist/plugins/host.d.ts +169 -0
  24. package/dist/plugins/host.js +585 -0
  25. package/dist/plugins/install.d.ts +100 -0
  26. package/dist/plugins/install.js +322 -0
  27. package/dist/providers/openai-compatible.d.ts +14 -0
  28. package/dist/providers/openai-compatible.js +228 -0
  29. package/dist/providers/registry.d.ts +35 -0
  30. package/dist/providers/registry.js +74 -0
  31. package/dist/providers/routing.d.ts +11 -0
  32. package/dist/providers/routing.js +41 -0
  33. package/dist/providers/settings.d.ts +30 -0
  34. package/dist/providers/settings.js +80 -0
  35. package/dist/resources/context.d.ts +44 -0
  36. package/dist/resources/context.js +171 -0
  37. package/dist/resources/prompts.d.ts +77 -0
  38. package/dist/resources/prompts.js +182 -0
  39. package/dist/resources/skills.d.ts +82 -0
  40. package/dist/resources/skills.js +214 -0
  41. package/dist/runtime/fs.d.ts +9 -0
  42. package/dist/runtime/fs.js +73 -0
  43. package/dist/runtime/modules.d.ts +16 -0
  44. package/dist/runtime/modules.js +83 -0
  45. package/dist/runtime/paths.d.ts +4 -0
  46. package/dist/runtime/paths.js +42 -0
  47. package/dist/runtime/process.d.ts +25 -0
  48. package/dist/runtime/process.js +122 -0
  49. package/dist/runtime/sqlite.d.ts +3 -0
  50. package/dist/runtime/sqlite.js +85 -0
  51. package/dist/runtime/store.d.ts +38 -0
  52. package/dist/runtime/store.js +259 -0
  53. package/dist/sessions/children.d.ts +55 -0
  54. package/dist/sessions/children.js +233 -0
  55. package/dist/startup/index.d.ts +41 -0
  56. package/dist/startup/index.js +105 -0
  57. package/dist/startup/mascot.d.ts +6 -0
  58. package/dist/startup/mascot.js +30 -0
  59. package/dist/startup/screen.d.ts +18 -0
  60. package/dist/startup/screen.js +117 -0
  61. package/dist/startup/text.d.ts +19 -0
  62. package/dist/startup/text.js +68 -0
  63. package/dist/tools/execute.d.ts +16 -0
  64. package/dist/tools/execute.js +96 -0
  65. package/dist/tools/search.d.ts +69 -0
  66. package/dist/tools/search.js +232 -0
  67. package/dist/tools/standard.d.ts +22 -0
  68. package/dist/tools/standard.js +419 -0
  69. package/dist/tools/webfetch.d.ts +26 -0
  70. package/dist/tools/webfetch.js +119 -0
  71. package/dist/trust.d.ts +34 -0
  72. package/dist/trust.js +113 -0
  73. package/dist/version.d.ts +7 -0
  74. package/dist/version.js +26 -0
  75. package/package.json +62 -0
@@ -0,0 +1,82 @@
1
+ export interface Skill {
2
+ /** Backward-compatible load id. */
3
+ name: string;
4
+ /** Stable catalog id; plugin skills are namespaced by their registering plugin. */
5
+ id: string;
6
+ effectiveId: string;
7
+ displayId: string;
8
+ description: string;
9
+ directory: string;
10
+ file: string;
11
+ scope: SkillScope;
12
+ source: string;
13
+ owner?: {
14
+ id: string;
15
+ name: string;
16
+ };
17
+ manageable: boolean;
18
+ locked: boolean;
19
+ enabled: boolean;
20
+ effective: boolean;
21
+ shadowedBy?: string;
22
+ approximateTokens: number;
23
+ }
24
+ export type SkillCatalogEntry = Omit<Skill, "directory" | "file">;
25
+ export type SkillScope = "project" | "config" | "user" | "plugin";
26
+ export interface SkillRoot {
27
+ dir: string;
28
+ scope: SkillScope;
29
+ source?: string;
30
+ owner?: {
31
+ id: string;
32
+ name: string;
33
+ };
34
+ }
35
+ /**
36
+ * Discovery roots in precedence order (first wins): project `.agents/skills`, `.alisio/skills`
37
+ * and `.claude/skills` (compat) from cwd up to the workspace root (trusted projects only), then
38
+ * explicitly configured paths, then `~/.agents/skills` and `<config home>/skills`, then plugins.
39
+ */
40
+ export declare function skillRoots(options: {
41
+ workspace: string;
42
+ cwd: string;
43
+ home: string;
44
+ configHome: string;
45
+ trusted: boolean;
46
+ configSkills?: string[];
47
+ pluginRoots?: Array<string | {
48
+ dir: string;
49
+ plugin: string;
50
+ name?: string;
51
+ }>;
52
+ }): SkillRoot[];
53
+ export declare class Skills {
54
+ items: Map<string, Skill>;
55
+ /** Every valid discovered skill, including lower-precedence shadowed entries. */
56
+ entries: Skill[];
57
+ diagnostics: string[];
58
+ private effective;
59
+ private overrides;
60
+ private seen;
61
+ private scanned;
62
+ private maxDepth;
63
+ private maxDirs;
64
+ constructor(options?: {
65
+ maxDepth?: number;
66
+ maxDirs?: number;
67
+ overrides?: Record<string, {
68
+ enabled: boolean;
69
+ }>;
70
+ });
71
+ /** Roots in precedence order; plain strings are treated as user-level roots. */
72
+ discover(roots: Array<string | SkillRoot>): Promise<void>;
73
+ private walk;
74
+ private parse;
75
+ catalog(): string;
76
+ /** Safe catalog metadata: no absolute paths and no skill body text. */
77
+ catalogEntries(): SkillCatalogEntry[];
78
+ setEnabled(id: string, enabled: boolean): Skill;
79
+ private refreshEnabledItems;
80
+ load(name: string): Promise<string>;
81
+ resource(name: string, path: string): Promise<string>;
82
+ }
@@ -0,0 +1,214 @@
1
+ import { readdir, realpath } from "node:fs/promises";
2
+ import { basename, dirname, join } from "node:path";
3
+ import { parse } from "yaml";
4
+ import { z } from "zod";
5
+ import { exists, fileSize, readText } from "../runtime/fs.js";
6
+ import { safePath } from "../runtime/paths.js";
7
+ /** Agent Skills name rule: 1-64 lowercase letters/digits, single hyphens, no edge hyphens. */
8
+ const metadata = z.object({
9
+ name: z
10
+ .string()
11
+ .max(64)
12
+ .regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "name must be lowercase letters, digits and single hyphens"),
13
+ description: z.string().min(1).max(1024),
14
+ });
15
+ /**
16
+ * Discovery roots in precedence order (first wins): project `.agents/skills`, `.alisio/skills`
17
+ * and `.claude/skills` (compat) from cwd up to the workspace root (trusted projects only), then
18
+ * explicitly configured paths, then `~/.agents/skills` and `<config home>/skills`, then plugins.
19
+ */
20
+ export function skillRoots(options) {
21
+ const roots = [];
22
+ if (options.trusted) {
23
+ let dir = options.cwd;
24
+ while (true) {
25
+ for (const convention of [".agents", ".alisio", ".claude"])
26
+ roots.push({ dir: join(dir, convention, "skills"), scope: "project" });
27
+ if (dir === options.workspace || dirname(dir) === dir)
28
+ break;
29
+ dir = dirname(dir);
30
+ }
31
+ }
32
+ for (const dir of options.configSkills ?? [])
33
+ roots.push({ dir, scope: "config", source: "configured" });
34
+ roots.push({ dir: join(options.home, ".agents", "skills"), scope: "user", source: "user" }, { dir: join(options.configHome, "skills"), scope: "user", source: "user" });
35
+ for (const value of options.pluginRoots ?? []) {
36
+ if (typeof value === "string")
37
+ roots.push({ dir: value, scope: "plugin", source: "plugin" });
38
+ else
39
+ roots.push({
40
+ dir: value.dir,
41
+ scope: "plugin",
42
+ source: "plugin",
43
+ owner: { id: value.plugin, name: value.name ?? value.plugin },
44
+ });
45
+ }
46
+ return roots;
47
+ }
48
+ export class Skills {
49
+ items = new Map();
50
+ /** Every valid discovered skill, including lower-precedence shadowed entries. */
51
+ entries = [];
52
+ diagnostics = [];
53
+ effective = new Map();
54
+ overrides;
55
+ seen = new Set();
56
+ scanned = 0;
57
+ maxDepth;
58
+ maxDirs;
59
+ constructor(options = {}) {
60
+ this.maxDepth = options.maxDepth ?? 5;
61
+ this.maxDirs = options.maxDirs ?? 2000;
62
+ this.overrides = { ...(options.overrides ?? {}) };
63
+ }
64
+ /** Roots in precedence order; plain strings are treated as user-level roots. */
65
+ async discover(roots) {
66
+ this.items.clear();
67
+ this.entries = [];
68
+ this.effective.clear();
69
+ this.diagnostics = [];
70
+ this.seen.clear();
71
+ this.scanned = 0;
72
+ for (const root of roots) {
73
+ const entry = typeof root === "string" ? { dir: root, scope: "user" } : root;
74
+ await this.walk(entry.dir, 0, entry);
75
+ }
76
+ this.refreshEnabledItems();
77
+ }
78
+ async walk(path, depth, root) {
79
+ if (depth > this.maxDepth)
80
+ return;
81
+ if (this.scanned >= this.maxDirs) {
82
+ if (this.scanned === this.maxDirs) {
83
+ this.diagnostics.push(`Skill scan limit of ${this.maxDirs} directories reached; stopped at ${path}`);
84
+ this.scanned++;
85
+ }
86
+ return;
87
+ }
88
+ let canonical;
89
+ try {
90
+ canonical = await realpath(path);
91
+ }
92
+ catch (e) {
93
+ if (["ENOENT", "ENOTDIR"].includes(e.code ?? ""))
94
+ return;
95
+ throw e;
96
+ }
97
+ if (this.seen.has(canonical))
98
+ return;
99
+ this.seen.add(canonical);
100
+ this.scanned++;
101
+ const file = join(canonical, "SKILL.md");
102
+ if (await exists(file)) {
103
+ try {
104
+ await safePath(canonical, file);
105
+ const data = await this.parse(file);
106
+ if (data.name !== basename(canonical))
107
+ this.diagnostics.push(`Skill ${data.name} lives in directory "${basename(canonical)}" (spec expects the same name); loaded anyway: ${file}`);
108
+ const existing = this.effective.get(data.name);
109
+ const displayId = root.owner ? `${root.owner.id}:${data.name}` : data.name;
110
+ const duplicate = this.entries.filter((entry) => entry.displayId === displayId).length;
111
+ const id = duplicate ? `${displayId}#shadow-${duplicate}` : displayId;
112
+ const size = (await fileSize(file)) ?? 0;
113
+ const item = {
114
+ ...data,
115
+ id,
116
+ effectiveId: existing?.effectiveId ?? id,
117
+ displayId,
118
+ directory: canonical,
119
+ file,
120
+ scope: root.scope,
121
+ source: root.source ?? root.scope,
122
+ ...(root.owner ? { owner: root.owner } : {}),
123
+ manageable: root.scope !== "plugin",
124
+ locked: root.scope === "plugin",
125
+ enabled: root.scope === "plugin" ? true : this.overrides[id]?.enabled !== false,
126
+ effective: !existing,
127
+ ...(existing ? { shadowedBy: existing.displayId } : {}),
128
+ approximateTokens: Math.max(1, Math.ceil(size / 4)),
129
+ };
130
+ this.entries.push(item);
131
+ if (existing)
132
+ this.diagnostics.push(`Skill ${data.name} from ${file} (${root.scope}) is shadowed: ${existing.file} (${existing.scope}) overrides it`);
133
+ else
134
+ this.effective.set(data.name, item);
135
+ }
136
+ catch (e) {
137
+ this.diagnostics.push(`${file}: ${e instanceof Error ? e.message : String(e)}`);
138
+ }
139
+ return;
140
+ }
141
+ let entries;
142
+ try {
143
+ entries = await readdir(canonical, { withFileTypes: true });
144
+ }
145
+ catch {
146
+ return;
147
+ }
148
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name)))
149
+ if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules")
150
+ await this.walk(join(canonical, entry.name), depth + 1, root);
151
+ }
152
+ async parse(file) {
153
+ if (((await fileSize(file)) ?? 0) > 64_000)
154
+ throw new Error("Skill exceeds 64 KB");
155
+ const text = await readText(file);
156
+ const match = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(text);
157
+ if (!match?.[1])
158
+ throw new Error("Missing YAML frontmatter");
159
+ return metadata.parse(parse(match[1], { maxAliasCount: 20 }));
160
+ }
161
+ catalog() {
162
+ const entries = [...this.items.values()].map((s) => ({
163
+ name: s.name,
164
+ description: s.description,
165
+ }));
166
+ const text = JSON.stringify(entries);
167
+ if (text.length > 24_000)
168
+ return "Large skill catalog. Use skill_search to find skills, then skill_load.";
169
+ return `Available skills (load instructions with skill_load):\n${text}`;
170
+ }
171
+ /** Safe catalog metadata: no absolute paths and no skill body text. */
172
+ catalogEntries() {
173
+ return this.entries.map(({ directory: _directory, file: _file, ...skill }) => ({
174
+ ...skill,
175
+ ...(skill.owner ? { owner: { ...skill.owner } } : {}),
176
+ }));
177
+ }
178
+ setEnabled(id, enabled) {
179
+ const skill = this.entries.find((entry) => entry.id === id && entry.effective);
180
+ if (!skill)
181
+ throw new Error(`Unknown effective skill: ${id}`);
182
+ if (!skill.manageable || skill.locked)
183
+ throw new Error("This skill is locked by its plugin; use /plugins to manage the owner");
184
+ skill.enabled = enabled;
185
+ this.overrides[id] = { enabled };
186
+ this.refreshEnabledItems();
187
+ return { ...skill };
188
+ }
189
+ refreshEnabledItems() {
190
+ this.items.clear();
191
+ for (const skill of this.effective.values())
192
+ if (skill.enabled)
193
+ this.items.set(skill.name, skill);
194
+ }
195
+ async load(name) {
196
+ const effective = this.effective.get(name) ?? this.entries.find((entry) => entry.id === name);
197
+ if (effective?.effective && !effective.enabled)
198
+ throw new Error(`Skill "${effective.displayId}" is disabled. Re-enable it with /skills.`);
199
+ const skill = this.items.get(name) ?? [...this.items.values()].find((entry) => entry.id === name);
200
+ if (!skill)
201
+ throw new Error(`Unknown skill: ${name}`);
202
+ await this.parse(skill.file);
203
+ return `Skill root: ${skill.directory}\nUse skill_resource to read supporting files.\n${await readText(skill.file)}`;
204
+ }
205
+ async resource(name, path) {
206
+ const skill = this.items.get(name);
207
+ if (!skill)
208
+ throw new Error("Unknown skill");
209
+ const file = await safePath(skill.directory, path);
210
+ if (((await fileSize(file)) ?? 0) > 32_000)
211
+ throw new Error("Resource exceeds 32 KB; split it into smaller files");
212
+ return readText(file);
213
+ }
214
+ }
@@ -0,0 +1,9 @@
1
+ export declare function fileSize(path: string): Promise<number | undefined>;
2
+ export declare const exists: (path: string) => Promise<boolean>;
3
+ /** Reads UTF-8 text, refusing files larger than `maxBytes`. */
4
+ export declare function readText(path: string, maxBytes?: number): Promise<string>;
5
+ export declare function readJson(path: string): Promise<unknown>;
6
+ /** First `bytes` bytes decoded as UTF-8. */
7
+ export declare function readHead(path: string, bytes: number): Promise<string>;
8
+ /** Executable lookup on PATH (PATHEXT on Windows), replacing Bun.which. */
9
+ export declare function which(command: string): Promise<string | undefined>;
@@ -0,0 +1,73 @@
1
+ /** Portable file helpers (node:fs) replacing Bun.file/Bun.write. */
2
+ import { constants } from "node:fs";
3
+ import { access, open, readFile, stat } from "node:fs/promises";
4
+ import { delimiter, isAbsolute, join } from "node:path";
5
+ const missing = (e) => ["ENOENT", "ENOTDIR"].includes(e.code ?? "");
6
+ export async function fileSize(path) {
7
+ try {
8
+ const s = await stat(path);
9
+ return s.isFile() ? s.size : undefined;
10
+ }
11
+ catch (e) {
12
+ if (missing(e))
13
+ return undefined;
14
+ throw e;
15
+ }
16
+ }
17
+ export const exists = async (path) => (await fileSize(path)) !== undefined;
18
+ /** Reads UTF-8 text, refusing files larger than `maxBytes`. */
19
+ export async function readText(path, maxBytes = Number.POSITIVE_INFINITY) {
20
+ const size = await fileSize(path);
21
+ if (size === undefined)
22
+ throw Object.assign(new Error(`File not found: ${path}`), { code: "ENOENT" });
23
+ if (size > maxBytes)
24
+ throw new Error(`File exceeds ${maxBytes} bytes: ${path}`);
25
+ return readFile(path, "utf8");
26
+ }
27
+ export async function readJson(path) {
28
+ try {
29
+ return JSON.parse(await readFile(path, "utf8"));
30
+ }
31
+ catch (e) {
32
+ if (missing(e))
33
+ return undefined;
34
+ throw e;
35
+ }
36
+ }
37
+ /** First `bytes` bytes decoded as UTF-8. */
38
+ export async function readHead(path, bytes) {
39
+ const handle = await open(path, "r");
40
+ try {
41
+ const buffer = Buffer.alloc(bytes);
42
+ const { bytesRead } = await handle.read(buffer, 0, bytes, 0);
43
+ return buffer.subarray(0, bytesRead).toString("utf8");
44
+ }
45
+ finally {
46
+ await handle.close();
47
+ }
48
+ }
49
+ /** Executable lookup on PATH (PATHEXT on Windows), replacing Bun.which. */
50
+ export async function which(command) {
51
+ const extensions = process.platform === "win32"
52
+ ? ["", ...(process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean)]
53
+ : [""];
54
+ const candidates = isAbsolute(command)
55
+ ? [command]
56
+ : (process.env.PATH ?? "")
57
+ .split(delimiter)
58
+ .filter(Boolean)
59
+ .map((dir) => join(dir, command));
60
+ for (const base of candidates)
61
+ for (const ext of extensions) {
62
+ const path = `${base}${ext}`;
63
+ try {
64
+ await access(path, process.platform === "win32" ? constants.F_OK : constants.X_OK);
65
+ if ((await fileSize(path)) !== undefined)
66
+ return path;
67
+ }
68
+ catch {
69
+ /* keep looking */
70
+ }
71
+ }
72
+ return undefined;
73
+ }
@@ -0,0 +1,16 @@
1
+ export declare const PLUGIN_KEYWORD = "alisio-plugin";
2
+ /** Paths start with `.`, are absolute, use `\`, end in a JS/TS extension or contain `/` unscoped. */
3
+ export declare function isPathSpec(spec: string): boolean;
4
+ /** Global package roots: NODE_PATH entries, the prefix of the running Node/npm, and the
5
+ * Alisio global plugins directory (`<configHome>/plugins`), where `alisio install` puts npm
6
+ * plugin packages. */
7
+ export declare function defaultGlobalRoots(): string[];
8
+ /**
9
+ * Returns the file to import (or the directory holding an alisio-plugin.json manifest).
10
+ * Packages are looked up from `from` upwards, then in global roots, and must declare the
11
+ * `alisio-plugin` keyword so a typo cannot load an unrelated package.
12
+ */
13
+ export declare function resolvePluginSpec(spec: string, options: {
14
+ from: string;
15
+ globalRoots?: string[];
16
+ }): Promise<string>;
@@ -0,0 +1,83 @@
1
+ /** Resolution of explicitly trusted plugins given as a path or an npm package name. */
2
+ import { dirname, isAbsolute, join, resolve } from "node:path";
3
+ import { configHome } from "../config.js";
4
+ import { readJson } from "./fs.js";
5
+ export const PLUGIN_KEYWORD = "alisio-plugin";
6
+ /** Paths start with `.`, are absolute, use `\`, end in a JS/TS extension or contain `/` unscoped. */
7
+ export function isPathSpec(spec) {
8
+ return (spec.startsWith(".") ||
9
+ isAbsolute(spec) ||
10
+ /^[A-Za-z]:[\\/]/.test(spec) ||
11
+ spec.includes("\\") ||
12
+ /\.(?:[cm]?[jt]s|tsx|jsx)$/.test(spec) ||
13
+ (!spec.startsWith("@") && spec.includes("/")));
14
+ }
15
+ /** Global package roots: NODE_PATH entries, the prefix of the running Node/npm, and the
16
+ * Alisio global plugins directory (`<configHome>/plugins`), where `alisio install` puts npm
17
+ * plugin packages. */
18
+ export function defaultGlobalRoots() {
19
+ const prefix = process.env.npm_config_prefix ?? process.env.PREFIX ?? dirname(dirname(process.execPath));
20
+ return [
21
+ ...(process.env.NODE_PATH ?? "")
22
+ .split(process.platform === "win32" ? ";" : ":")
23
+ .filter(Boolean),
24
+ process.platform === "win32"
25
+ ? join(prefix, "node_modules")
26
+ : join(prefix, "lib", "node_modules"),
27
+ join(configHome(), "plugins"),
28
+ ];
29
+ }
30
+ function entryOf(manifest) {
31
+ const exp = manifest.exports;
32
+ const pick = (value) => {
33
+ if (typeof value === "string")
34
+ return value;
35
+ if (value && typeof value === "object") {
36
+ const record = value;
37
+ for (const key of ["import", "node", "default"]) {
38
+ const found = pick(record[key]);
39
+ if (found)
40
+ return found;
41
+ }
42
+ }
43
+ return undefined;
44
+ };
45
+ const root = exp && typeof exp === "object" && !Array.isArray(exp) && "." in exp
46
+ ? exp["."]
47
+ : exp;
48
+ return pick(root) ?? manifest.main ?? "./index.js";
49
+ }
50
+ /**
51
+ * Returns the file to import (or the directory holding an alisio-plugin.json manifest).
52
+ * Packages are looked up from `from` upwards, then in global roots, and must declare the
53
+ * `alisio-plugin` keyword so a typo cannot load an unrelated package.
54
+ */
55
+ export async function resolvePluginSpec(spec, options) {
56
+ if (isPathSpec(spec))
57
+ return resolve(options.from, spec);
58
+ const searched = [];
59
+ const candidates = [];
60
+ for (let dir = resolve(options.from);; dir = dirname(dir)) {
61
+ candidates.push(join(dir, "node_modules", spec));
62
+ if (dirname(dir) === dir)
63
+ break;
64
+ }
65
+ for (const rootDir of options.globalRoots ?? defaultGlobalRoots()) {
66
+ candidates.push(join(rootDir, spec));
67
+ // `alisio install` writes npm packages under <root>/node_modules/<spec>.
68
+ candidates.push(join(rootDir, "node_modules", spec));
69
+ }
70
+ for (const dir of candidates) {
71
+ searched.push(dir);
72
+ const manifest = (await readJson(join(dir, "package.json")));
73
+ if (!manifest)
74
+ continue;
75
+ const keywords = Array.isArray(manifest.keywords) ? manifest.keywords : [];
76
+ if (!keywords.includes(PLUGIN_KEYWORD))
77
+ throw new Error(`Package ${spec} is not an Alisio plugin (missing "${PLUGIN_KEYWORD}" keyword)`);
78
+ if (await readJson(join(dir, "alisio-plugin.json")))
79
+ return dir;
80
+ return resolve(dir, entryOf(manifest));
81
+ }
82
+ throw new Error(`Plugin package ${spec} not found (searched ${searched.length} locations from ${options.from})`);
83
+ }
@@ -0,0 +1,4 @@
1
+ export declare function inside(root: string, path: string): boolean;
2
+ /** Mediated path policy; not an OS sandbox and not race-proof against hostile concurrent processes. */
3
+ export declare function safePath(root: string, path: string): Promise<string>;
4
+ export declare function findWorkspace(cwd: string): Promise<string>;
@@ -0,0 +1,42 @@
1
+ import { lstat, realpath } from "node:fs/promises";
2
+ import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
3
+ export function inside(root, path) {
4
+ const r = relative(root, path);
5
+ return r === "" || (!r.startsWith(`..${sep}`) && r !== ".." && !isAbsolute(r));
6
+ }
7
+ /** Mediated path policy; not an OS sandbox and not race-proof against hostile concurrent processes. */
8
+ export async function safePath(root, path) {
9
+ const canonical = await realpath(root), target = resolve(canonical, path);
10
+ if (!inside(canonical, target))
11
+ throw new Error("Path outside workspace");
12
+ let cursor = canonical;
13
+ for (const part of relative(canonical, target).split(sep).filter(Boolean)) {
14
+ cursor = resolve(cursor, part);
15
+ try {
16
+ const stat = await lstat(cursor);
17
+ if (stat.isSymbolicLink())
18
+ throw new Error(`Symlinks are not allowed: ${cursor}`);
19
+ }
20
+ catch (e) {
21
+ if (e.code !== "ENOENT")
22
+ throw e;
23
+ }
24
+ }
25
+ return target;
26
+ }
27
+ export async function findWorkspace(cwd) {
28
+ let path = await realpath(cwd);
29
+ while (true) {
30
+ try {
31
+ await lstat(resolve(path, ".git"));
32
+ return path;
33
+ }
34
+ catch (e) {
35
+ if (e.code !== "ENOENT")
36
+ throw e;
37
+ }
38
+ if (dirname(path) === path)
39
+ return realpath(cwd);
40
+ path = dirname(path);
41
+ }
42
+ }
@@ -0,0 +1,25 @@
1
+ export interface ProcessResult {
2
+ stdout: string;
3
+ stderr: string;
4
+ exitCode: number;
5
+ truncated: boolean;
6
+ }
7
+ /**
8
+ * Detect a spawn failure caused by a missing executable (`ENOENT`), so tools can explain how to
9
+ * install the dependency instead of surfacing the raw OS error.
10
+ */
11
+ export declare function isMissingCommand(error: unknown): boolean;
12
+ /** Per-platform install commands for the ripgrep dependency. */
13
+ export declare const RIPGREP_INSTALL_HINT: string;
14
+ /** Human-readable detection status for `alisio doctor`. */
15
+ export declare function ripgrepDiagnostic(found: boolean): string | undefined;
16
+ export declare function runProcess(command: string, args: string[], options: {
17
+ cwd: string;
18
+ signal: AbortSignal;
19
+ timeoutMs?: number;
20
+ maxBytes?: number;
21
+ env?: Record<string, string>;
22
+ onData?: (chunk: string) => void;
23
+ /** On cancellation: SIGTERM the process group, then SIGKILL after this grace (default 5s). */
24
+ killGraceMs?: number;
25
+ }): Promise<ProcessResult>;
@@ -0,0 +1,122 @@
1
+ import { spawn, spawnSync } from "node:child_process";
2
+ /**
3
+ * Detect a spawn failure caused by a missing executable (`ENOENT`), so tools can explain how to
4
+ * install the dependency instead of surfacing the raw OS error.
5
+ */
6
+ export function isMissingCommand(error) {
7
+ return (!!error &&
8
+ typeof error === "object" &&
9
+ "code" in error &&
10
+ error.code === "ENOENT");
11
+ }
12
+ /** Per-platform install commands for the ripgrep dependency. */
13
+ export const RIPGREP_INSTALL_HINT = "Install ripgrep (rg): `sudo apt install ripgrep` (Debian/Ubuntu), " +
14
+ "`brew install ripgrep` (macOS), `winget install BurntSushi.ripgrep.MSVC` or " +
15
+ "`scoop install ripgrep` (Windows). It powers the search_text and list_files tools.";
16
+ /** Human-readable detection status for `alisio doctor`. */
17
+ export function ripgrepDiagnostic(found) {
18
+ return found ? undefined : RIPGREP_INSTALL_HINT;
19
+ }
20
+ export async function runProcess(command, args, options) {
21
+ options.signal.throwIfAborted();
22
+ const signal = AbortSignal.any([
23
+ options.signal,
24
+ AbortSignal.timeout(options.timeoutMs ?? 30_000),
25
+ ]);
26
+ const child = spawn(command, args, {
27
+ cwd: options.cwd,
28
+ env: options.env ??
29
+ Object.fromEntries(["PATH", "HOME", "USERPROFILE", "TMPDIR", "TEMP", "SystemRoot", "COMSPEC", "LANG"].flatMap((k) => (process.env[k] ? [[k, process.env[k]]] : []))),
30
+ stdio: ["ignore", "pipe", "pipe"],
31
+ detached: process.platform !== "win32",
32
+ windowsHide: true,
33
+ });
34
+ const exited = new Promise((resolveExit, reject) => {
35
+ child.once("error", reject);
36
+ child.once("close", (code, sig) => resolveExit(code ?? (sig ? 128 : 1)));
37
+ });
38
+ let finished = false;
39
+ exited.then(() => {
40
+ finished = true;
41
+ }, () => {
42
+ finished = true;
43
+ });
44
+ let graceTimer;
45
+ /** Graceful stop for cancellations: SIGTERM the group, SIGKILL if still alive after the grace. */
46
+ const terminate = () => {
47
+ if (finished || child.pid === undefined)
48
+ return;
49
+ if (process.platform === "win32")
50
+ return kill();
51
+ try {
52
+ process.kill(-child.pid, "SIGTERM");
53
+ }
54
+ catch {
55
+ return kill();
56
+ }
57
+ graceTimer = setTimeout(kill, options.killGraceMs ?? 5_000);
58
+ };
59
+ const kill = () => {
60
+ if (finished || child.pid === undefined)
61
+ return;
62
+ try {
63
+ if (process.platform !== "win32")
64
+ process.kill(-child.pid, "SIGKILL");
65
+ else {
66
+ spawnSync("taskkill", ["/PID", String(child.pid), "/T", "/F"], { stdio: "ignore" });
67
+ child.kill();
68
+ }
69
+ }
70
+ catch {
71
+ child.kill("SIGKILL");
72
+ }
73
+ };
74
+ signal.addEventListener("abort", terminate, { once: true });
75
+ if (signal.aborted)
76
+ terminate();
77
+ let size = 0, truncated = false;
78
+ const max = options.maxBytes ?? 32_000;
79
+ const consume = (stream) => new Promise((resolveText, reject) => {
80
+ const decoder = new TextDecoder();
81
+ let text = "", done = false;
82
+ const finish = () => {
83
+ if (done)
84
+ return;
85
+ done = true;
86
+ text += decoder.decode();
87
+ resolveText(text);
88
+ };
89
+ stream.on("data", (value) => {
90
+ if (done)
91
+ return;
92
+ const remaining = Math.max(0, max - size), slice = value.subarray(0, remaining);
93
+ size += slice.byteLength;
94
+ const chunk = decoder.decode(slice, { stream: true });
95
+ text += chunk;
96
+ if (chunk)
97
+ options.onData?.(chunk);
98
+ if (value.byteLength > remaining) {
99
+ truncated = true;
100
+ kill();
101
+ finish();
102
+ }
103
+ });
104
+ stream.once("end", finish);
105
+ stream.once("close", finish);
106
+ stream.once("error", reject);
107
+ });
108
+ try {
109
+ const [stdout, stderr, exitCode] = await Promise.all([
110
+ consume(child.stdout),
111
+ consume(child.stderr),
112
+ exited,
113
+ ]);
114
+ signal.throwIfAborted();
115
+ return { stdout, stderr, exitCode, truncated };
116
+ }
117
+ finally {
118
+ signal.removeEventListener("abort", terminate);
119
+ clearTimeout(graceTimer);
120
+ kill();
121
+ }
122
+ }
@@ -0,0 +1,3 @@
1
+ import type { SqlDatabase } from "@alisio/sdk";
2
+ export declare function isSqliteExperimentalWarning(message: string, type?: string): boolean;
3
+ export declare function openDatabase(path: string): SqlDatabase;