keystone-cli 1.0.1 → 1.0.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/README.md +27 -4
- package/package.json +1 -1
- package/src/cli.ts +188 -3
- package/src/db/memory-db.ts +51 -4
- package/src/db/sqlite-setup.ts +2 -3
- package/src/runner/llm-adapter.ts +295 -2
- package/src/utils/workflow-registry.test.ts +2 -2
package/README.md
CHANGED
|
@@ -133,14 +133,17 @@ keystone ui
|
|
|
133
133
|
|
|
134
134
|
`keystone init` seeds these workflows under `.keystone/workflows/` (and the agents they rely on under `.keystone/workflows/agents/`):
|
|
135
135
|
|
|
136
|
+
Top-level workflows:
|
|
136
137
|
- `scaffold-feature`: Interactive workflow scaffolder. Prompts for requirements, plans files, generates content, and writes them.
|
|
138
|
+
- `decompose-problem`: Decomposes a problem into research/implementation/review tasks, waits for approval, runs sub-workflows, and summarizes.
|
|
139
|
+
- `dev`: Self-bootstrapping DevMode workflow for an interactive plan/implement/verify loop.
|
|
140
|
+
|
|
141
|
+
Sub-workflows:
|
|
137
142
|
- `scaffold-plan`: Generates a file plan from `requirements` input.
|
|
138
143
|
- `scaffold-generate`: Generates file contents from `requirements` plus a `files` plan.
|
|
139
|
-
- `decompose-problem`: Decomposes a problem into research/implementation/review tasks, waits for approval, runs sub-workflows, and summarizes.
|
|
140
144
|
- `decompose-research`: Runs a single research task (`task`) with optional `context`/`constraints`.
|
|
141
145
|
- `decompose-implement`: Runs a single implementation task (`task`) with optional `research` findings.
|
|
142
146
|
- `decompose-review`: Reviews a single implementation task (`task`) with optional `implementation` results.
|
|
143
|
-
- `dev`: Self-bootstrapping DevMode workflow for an interactive plan/implement/verify loop.
|
|
144
147
|
|
|
145
148
|
Example runs:
|
|
146
149
|
```bash
|
|
@@ -148,7 +151,7 @@ keystone run scaffold-feature
|
|
|
148
151
|
keystone run decompose-problem -i problem="Add caching to the API" -i context="Node/Bun service"
|
|
149
152
|
```
|
|
150
153
|
|
|
151
|
-
|
|
154
|
+
Sub-workflows are used by the top-level workflows, but can be run directly if you want just one phase.
|
|
152
155
|
|
|
153
156
|
---
|
|
154
157
|
|
|
@@ -258,7 +261,7 @@ model: claude-3-5-sonnet-latest
|
|
|
258
261
|
```
|
|
259
262
|
|
|
260
263
|
### OpenAI Compatible Providers
|
|
261
|
-
You can add any OpenAI-compatible provider (
|
|
264
|
+
You can add any OpenAI-compatible provider (Together AI, Perplexity, Local Ollama, etc.) by setting the `type` to `openai` and providing the `base_url` and `api_key_env`.
|
|
262
265
|
|
|
263
266
|
### GitHub Copilot Support
|
|
264
267
|
|
|
@@ -909,6 +912,15 @@ In these examples, the agent will have access to all tools provided by the MCP s
|
|
|
909
912
|
|
|
910
913
|
---
|
|
911
914
|
|
|
915
|
+
### Compile
|
|
916
|
+
`keystone compile -o ./keystone-app` emits the executable plus a `keystone-runtime/` directory next to it.
|
|
917
|
+
Ship both together if you use memory/embeddings (the runtime folder includes native deps like ONNX Runtime,
|
|
918
|
+
sqlite-vec, and sharp). The compile step also copies native shared libraries (for example `libonnxruntime.*`
|
|
919
|
+
and `vec0.*`) next to the binary. You can move the runtime folder and set `KEYSTONE_RUNTIME_DIR` to point
|
|
920
|
+
to it. If you move the ONNX Runtime library elsewhere, set `KEYSTONE_ONNX_RUNTIME_LIB_DIR` to that directory.
|
|
921
|
+
If you do not use memory/embeddings, the binary alone is sufficient. If you see cache warnings from local
|
|
922
|
+
embeddings in a compiled run, set `TRANSFORMERS_CACHE` to a writable directory.
|
|
923
|
+
|
|
912
924
|
Input keys passed via `-i key=val` must be alphanumeric/underscore and cannot be `__proto__`, `constructor`, or `prototype`.
|
|
913
925
|
|
|
914
926
|
### Dry Run
|
|
@@ -956,11 +968,18 @@ graph TD
|
|
|
956
968
|
SE --> File[File Operations]
|
|
957
969
|
SE --> HTTP[HTTP Requests]
|
|
958
970
|
SE --> Human[Human Input]
|
|
971
|
+
SE --> Engine[Engine Executor]
|
|
972
|
+
SE --> Script[Script Step]
|
|
973
|
+
SE --> Sleep[Sleep Step]
|
|
974
|
+
SE --> Memory[Memory operations]
|
|
975
|
+
SE --> Workflow[Sub-workflows]
|
|
959
976
|
LLM --> Adapters[LLM Adapters]
|
|
960
977
|
Adapters --> OpenAI
|
|
961
978
|
Adapters --> Anthropic
|
|
979
|
+
Adapters --> Gemini
|
|
962
980
|
Adapters --> Copilot
|
|
963
981
|
Adapters --> ChatGPT
|
|
982
|
+
Adapters --> Local
|
|
964
983
|
LLM --> MCPClient[MCP Client]
|
|
965
984
|
WR --> Eval[Expression Evaluator]
|
|
966
985
|
WR --> Pool[Resource Pool Manager]
|
|
@@ -968,12 +987,16 @@ graph TD
|
|
|
968
987
|
|
|
969
988
|
## 📂 Project Structure
|
|
970
989
|
|
|
990
|
+
- `src/cli.ts`: CLI entry point.
|
|
971
991
|
- `src/db/`: SQLite persistence layer.
|
|
972
992
|
- `src/runner/`: The core execution engine, handles parallelization and retries.
|
|
973
993
|
- `src/parser/`: Zod-powered validation for workflows and agents.
|
|
974
994
|
- `src/expression/`: `${{ }}` expression evaluator.
|
|
995
|
+
- `src/templates/`: Bundled workflow and agent templates.
|
|
975
996
|
- `src/ui/`: Ink-powered TUI dashboard.
|
|
976
997
|
- `src/utils/`: Shared utilities (auth, redaction, config loading).
|
|
998
|
+
- `src/types/`: Core type definitions.
|
|
999
|
+
- `src/e2e-tests/`: End-to-end test suite.
|
|
977
1000
|
- `.keystone/workflows/`: Your YAML workflow definitions.
|
|
978
1001
|
|
|
979
1002
|
---
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -773,10 +773,23 @@ program
|
|
|
773
773
|
.option('--project <path>', 'Project directory (default: .)', '.')
|
|
774
774
|
.action(async (options) => {
|
|
775
775
|
const { spawnSync } = await import('node:child_process');
|
|
776
|
-
const {
|
|
777
|
-
const {
|
|
776
|
+
const { createRequire } = await import('node:module');
|
|
777
|
+
const { resolve, join, dirname } = await import('node:path');
|
|
778
|
+
const {
|
|
779
|
+
copyFileSync,
|
|
780
|
+
existsSync,
|
|
781
|
+
lstatSync,
|
|
782
|
+
mkdirSync,
|
|
783
|
+
readdirSync,
|
|
784
|
+
readFileSync,
|
|
785
|
+
readlinkSync,
|
|
786
|
+
rmSync,
|
|
787
|
+
symlinkSync,
|
|
788
|
+
} = await import('node:fs');
|
|
778
789
|
|
|
779
790
|
const projectDir = resolve(options.project);
|
|
791
|
+
const outputPath = resolve(options.outfile);
|
|
792
|
+
const outputDir = dirname(outputPath);
|
|
780
793
|
const keystoneDir = join(projectDir, '.keystone');
|
|
781
794
|
|
|
782
795
|
if (!existsSync(keystoneDir)) {
|
|
@@ -790,7 +803,151 @@ program
|
|
|
790
803
|
// Find the CLI source path
|
|
791
804
|
const cliSource = resolve(import.meta.dir, 'cli.ts');
|
|
792
805
|
|
|
793
|
-
const
|
|
806
|
+
const osName = process.platform === 'win32' ? 'windows' : process.platform;
|
|
807
|
+
const externalPackages: string[] = [];
|
|
808
|
+
|
|
809
|
+
const buildArgs = ['build', cliSource, '--compile', '--outfile', outputPath];
|
|
810
|
+
for (const pkg of externalPackages) {
|
|
811
|
+
buildArgs.push('--external', pkg);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
const copyOnnxRuntimeLibs = (outfile: string): { copied: number; checked: boolean } => {
|
|
815
|
+
const runtimeDir = join(
|
|
816
|
+
projectDir,
|
|
817
|
+
'node_modules',
|
|
818
|
+
'onnxruntime-node',
|
|
819
|
+
'bin',
|
|
820
|
+
'napi-v3',
|
|
821
|
+
process.platform,
|
|
822
|
+
process.arch
|
|
823
|
+
);
|
|
824
|
+
if (!existsSync(runtimeDir)) return { copied: 0, checked: false };
|
|
825
|
+
|
|
826
|
+
const entries = readdirSync(runtimeDir, { withFileTypes: true });
|
|
827
|
+
const libPattern =
|
|
828
|
+
process.platform === 'win32' ? /^onnxruntime.*\.dll$/i : /^libonnxruntime/i;
|
|
829
|
+
let copied = 0;
|
|
830
|
+
|
|
831
|
+
for (const entry of entries) {
|
|
832
|
+
if (!entry.isFile() || !libPattern.test(entry.name)) continue;
|
|
833
|
+
copyFileSync(join(runtimeDir, entry.name), join(dirname(outfile), entry.name));
|
|
834
|
+
copied += 1;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
return { copied, checked: true };
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
const copyDir = (source: string, destination: string): void => {
|
|
841
|
+
const stats = lstatSync(source);
|
|
842
|
+
if (stats.isSymbolicLink()) {
|
|
843
|
+
const linkTarget = readlinkSync(source);
|
|
844
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
845
|
+
symlinkSync(linkTarget, destination);
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
if (stats.isDirectory()) {
|
|
849
|
+
mkdirSync(destination, { recursive: true });
|
|
850
|
+
for (const entry of readdirSync(source, { withFileTypes: true })) {
|
|
851
|
+
copyDir(join(source, entry.name), join(destination, entry.name));
|
|
852
|
+
}
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
if (stats.isFile()) {
|
|
856
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
857
|
+
copyFileSync(source, destination);
|
|
858
|
+
}
|
|
859
|
+
};
|
|
860
|
+
|
|
861
|
+
const copyRuntimeDependencies = (outfile: string): { copied: number; missing: string[] } => {
|
|
862
|
+
const runtimeDir = join(dirname(outfile), 'keystone-runtime');
|
|
863
|
+
const runtimeNodeModules = join(runtimeDir, 'node_modules');
|
|
864
|
+
rmSync(runtimeDir, { recursive: true, force: true });
|
|
865
|
+
mkdirSync(runtimeNodeModules, { recursive: true });
|
|
866
|
+
|
|
867
|
+
const roots = [
|
|
868
|
+
'@xenova/transformers',
|
|
869
|
+
'onnxruntime-node',
|
|
870
|
+
'onnxruntime-common',
|
|
871
|
+
'sharp',
|
|
872
|
+
'@huggingface/jinja',
|
|
873
|
+
'sqlite-vec',
|
|
874
|
+
`sqlite-vec-${osName}-${process.arch}`,
|
|
875
|
+
];
|
|
876
|
+
|
|
877
|
+
const require = createRequire(import.meta.url);
|
|
878
|
+
const resolvePackageDir = (pkg: string): string | null => {
|
|
879
|
+
try {
|
|
880
|
+
const pkgJson = require.resolve(`${pkg}/package.json`, { paths: [projectDir] });
|
|
881
|
+
return dirname(pkgJson);
|
|
882
|
+
} catch {
|
|
883
|
+
return null;
|
|
884
|
+
}
|
|
885
|
+
};
|
|
886
|
+
|
|
887
|
+
const queue = [...roots];
|
|
888
|
+
const seen = new Set<string>();
|
|
889
|
+
const missing: string[] = [];
|
|
890
|
+
let copied = 0;
|
|
891
|
+
|
|
892
|
+
while (queue.length) {
|
|
893
|
+
const pkg = queue.shift();
|
|
894
|
+
if (!pkg || seen.has(pkg)) continue;
|
|
895
|
+
seen.add(pkg);
|
|
896
|
+
|
|
897
|
+
const pkgDir = resolvePackageDir(pkg);
|
|
898
|
+
if (!pkgDir) {
|
|
899
|
+
missing.push(pkg);
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
const destDir = join(runtimeNodeModules, ...pkg.split('/'));
|
|
904
|
+
copyDir(pkgDir, destDir);
|
|
905
|
+
copied += 1;
|
|
906
|
+
|
|
907
|
+
try {
|
|
908
|
+
const pkgJsonPath = join(pkgDir, 'package.json');
|
|
909
|
+
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf8')) as {
|
|
910
|
+
dependencies?: Record<string, string>;
|
|
911
|
+
optionalDependencies?: Record<string, string>;
|
|
912
|
+
};
|
|
913
|
+
for (const dep of Object.keys(pkgJson.dependencies || {})) {
|
|
914
|
+
if (!seen.has(dep)) {
|
|
915
|
+
queue.push(dep);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
for (const dep of Object.keys(pkgJson.optionalDependencies || {})) {
|
|
919
|
+
if (seen.has(dep)) continue;
|
|
920
|
+
if (resolvePackageDir(dep)) {
|
|
921
|
+
queue.push(dep);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
} catch {
|
|
925
|
+
// Ignore dependency parsing errors.
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
return { copied, missing };
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
const copySqliteVecLib = (outfile: string): { copied: number; checked: boolean } => {
|
|
933
|
+
const osName = process.platform === 'win32' ? 'windows' : process.platform;
|
|
934
|
+
const extension =
|
|
935
|
+
process.platform === 'win32' ? 'dll' : process.platform === 'darwin' ? 'dylib' : 'so';
|
|
936
|
+
const sqliteVecDir = join(projectDir, 'node_modules', `sqlite-vec-${osName}-${process.arch}`);
|
|
937
|
+
if (!existsSync(sqliteVecDir)) return { copied: 0, checked: false };
|
|
938
|
+
|
|
939
|
+
const entries = readdirSync(sqliteVecDir, { withFileTypes: true });
|
|
940
|
+
const targetName = `vec0.${extension}`;
|
|
941
|
+
let copied = 0;
|
|
942
|
+
|
|
943
|
+
for (const entry of entries) {
|
|
944
|
+
if (!entry.isFile() || entry.name !== targetName) continue;
|
|
945
|
+
copyFileSync(join(sqliteVecDir, entry.name), join(dirname(outfile), entry.name));
|
|
946
|
+
copied += 1;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
return { copied, checked: true };
|
|
950
|
+
};
|
|
794
951
|
|
|
795
952
|
console.log(`🚀 Running: ASSETS_DIR=${keystoneDir} bun ${buildArgs.join(' ')}`);
|
|
796
953
|
|
|
@@ -803,6 +960,34 @@ program
|
|
|
803
960
|
});
|
|
804
961
|
|
|
805
962
|
if (result.status === 0) {
|
|
963
|
+
const { copied, checked } = copyOnnxRuntimeLibs(outputPath);
|
|
964
|
+
if (copied > 0) {
|
|
965
|
+
console.log(`📦 Copied ${copied} ONNX Runtime library file(s) next to ${outputPath}`);
|
|
966
|
+
} else if (checked) {
|
|
967
|
+
console.log(
|
|
968
|
+
'ℹ️ ONNX Runtime library not found; local embeddings may require external setup.'
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
const runtimeDeps = copyRuntimeDependencies(outputPath);
|
|
972
|
+
if (runtimeDeps.copied > 0) {
|
|
973
|
+
console.log(
|
|
974
|
+
`📦 Copied ${runtimeDeps.copied} runtime package(s) to ${join(
|
|
975
|
+
outputDir,
|
|
976
|
+
'keystone-runtime'
|
|
977
|
+
)}`
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
if (runtimeDeps.missing.length > 0) {
|
|
981
|
+
console.log(`ℹ️ Missing runtime packages: ${runtimeDeps.missing.join(', ')}`);
|
|
982
|
+
}
|
|
983
|
+
const sqliteVecStatus = copySqliteVecLib(outputPath);
|
|
984
|
+
if (sqliteVecStatus.copied > 0) {
|
|
985
|
+
console.log(
|
|
986
|
+
`📦 Copied ${sqliteVecStatus.copied} sqlite-vec extension file(s) next to ${outputPath}`
|
|
987
|
+
);
|
|
988
|
+
} else if (sqliteVecStatus.checked) {
|
|
989
|
+
console.log('ℹ️ sqlite-vec extension not found; memory steps may fail.');
|
|
990
|
+
}
|
|
806
991
|
console.log(`\n✨ Successfully compiled to ${options.outfile}`);
|
|
807
992
|
console.log(` You can now run ./${options.outfile} anywhere!`);
|
|
808
993
|
} else {
|
package/src/db/memory-db.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Database } from 'bun:sqlite';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { existsSync, mkdirSync } from 'node:fs';
|
|
4
|
-
import { dirname } from 'node:path';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
5
|
import * as sqliteVec from 'sqlite-vec';
|
|
6
6
|
import './sqlite-setup.ts';
|
|
7
7
|
|
|
@@ -12,6 +12,54 @@ export interface MemoryEntry {
|
|
|
12
12
|
distance?: number;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
const SQLITE_VEC_EXTENSION =
|
|
16
|
+
process.platform === 'win32' ? 'dll' : process.platform === 'darwin' ? 'dylib' : 'so';
|
|
17
|
+
const SQLITE_VEC_FILENAME = `vec0.${SQLITE_VEC_EXTENSION}`;
|
|
18
|
+
|
|
19
|
+
function getRuntimeDir(): string {
|
|
20
|
+
return process.env.KEYSTONE_RUNTIME_DIR || join(dirname(process.execPath), 'keystone-runtime');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function resolveSqliteVecPath(): string {
|
|
24
|
+
const overridePath = process.env.KEYSTONE_SQLITE_VEC_PATH;
|
|
25
|
+
if (overridePath && existsSync(overridePath)) {
|
|
26
|
+
return overridePath;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const loadablePath = sqliteVec.getLoadablePath();
|
|
31
|
+
if (existsSync(loadablePath)) {
|
|
32
|
+
return loadablePath;
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
// Fall through to additional lookup paths.
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const osName = process.platform === 'win32' ? 'windows' : process.platform;
|
|
39
|
+
const runtimeDir = getRuntimeDir();
|
|
40
|
+
const candidatePaths = [
|
|
41
|
+
join(runtimeDir, 'node_modules', `sqlite-vec-${osName}-${process.arch}`, SQLITE_VEC_FILENAME),
|
|
42
|
+
join(
|
|
43
|
+
process.cwd(),
|
|
44
|
+
'node_modules',
|
|
45
|
+
`sqlite-vec-${osName}-${process.arch}`,
|
|
46
|
+
SQLITE_VEC_FILENAME
|
|
47
|
+
),
|
|
48
|
+
join(dirname(process.execPath), SQLITE_VEC_FILENAME),
|
|
49
|
+
join(dirname(process.execPath), 'lib', SQLITE_VEC_FILENAME),
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
for (const candidate of candidatePaths) {
|
|
53
|
+
if (existsSync(candidate)) {
|
|
54
|
+
return candidate;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
throw new Error(
|
|
59
|
+
`Loadable extension for sqlite-vec not found. Set KEYSTONE_SQLITE_VEC_PATH or install sqlite-vec-${osName}-${process.arch}.`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
15
63
|
export class MemoryDb {
|
|
16
64
|
private db: Database;
|
|
17
65
|
// Cache connections by path to avoid reloading extensions
|
|
@@ -24,7 +72,6 @@ export class MemoryDb {
|
|
|
24
72
|
cached.refCount++;
|
|
25
73
|
this.db = cached.db;
|
|
26
74
|
} else {
|
|
27
|
-
const { Database } = require('bun:sqlite');
|
|
28
75
|
const dir = dirname(dbPath);
|
|
29
76
|
if (!existsSync(dir)) {
|
|
30
77
|
mkdirSync(dir, { recursive: true });
|
|
@@ -32,7 +79,7 @@ export class MemoryDb {
|
|
|
32
79
|
this.db = new Database(dbPath, { create: true });
|
|
33
80
|
|
|
34
81
|
// Load sqlite-vec extension
|
|
35
|
-
const extensionPath =
|
|
82
|
+
const extensionPath = resolveSqliteVecPath();
|
|
36
83
|
this.db.loadExtension(extensionPath);
|
|
37
84
|
|
|
38
85
|
this.initSchema();
|
package/src/db/sqlite-setup.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
1
3
|
import { ConsoleLogger, type Logger } from '../utils/logger.ts';
|
|
2
4
|
|
|
3
5
|
export function setupSqlite(logger: Logger = new ConsoleLogger()) {
|
|
@@ -5,9 +7,6 @@ export function setupSqlite(logger: Logger = new ConsoleLogger()) {
|
|
|
5
7
|
// We need to try to load a custom one (e.g. from Homebrew) if on macOS
|
|
6
8
|
if (process.platform === 'darwin') {
|
|
7
9
|
try {
|
|
8
|
-
const { Database } = require('bun:sqlite');
|
|
9
|
-
const { existsSync } = require('node:fs');
|
|
10
|
-
|
|
11
10
|
// Common Homebrew paths for SQLite
|
|
12
11
|
const paths = [
|
|
13
12
|
'/opt/homebrew/opt/sqlite/lib/libsqlite3.dylib',
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { copyFileSync, existsSync, readdirSync } from 'node:fs';
|
|
3
|
+
import { Module } from 'node:module';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { basename, dirname, join } from 'node:path';
|
|
6
|
+
import { pathToFileURL } from 'node:url';
|
|
3
7
|
import { AuthManager, COPILOT_HEADERS } from '../utils/auth-manager';
|
|
4
8
|
import { ConfigLoader } from '../utils/config-loader';
|
|
5
9
|
import { ConsoleLogger } from '../utils/logger';
|
|
@@ -22,6 +26,286 @@ const GEMINI_HEADERS = {
|
|
|
22
26
|
'{"ideType":"IDE_UNSPECIFIED","platform":"PLATFORM_UNSPECIFIED","pluginType":"GEMINI"}',
|
|
23
27
|
};
|
|
24
28
|
const defaultLogger = new ConsoleLogger();
|
|
29
|
+
type TransformersPipeline = (...args: unknown[]) => Promise<unknown>;
|
|
30
|
+
let cachedPipeline: TransformersPipeline | null = null;
|
|
31
|
+
let runtimeResolverRegistered = false;
|
|
32
|
+
let nativeFallbacksRegistered = false;
|
|
33
|
+
|
|
34
|
+
const ONNX_RUNTIME_LIB_PATTERN =
|
|
35
|
+
process.platform === 'win32' ? /^onnxruntime.*\.dll$/i : /^libonnxruntime/i;
|
|
36
|
+
|
|
37
|
+
function hasOnnxRuntimeLibrary(dir: string): boolean {
|
|
38
|
+
try {
|
|
39
|
+
return readdirSync(dir, { withFileTypes: true }).some(
|
|
40
|
+
(entry) => entry.isFile() && ONNX_RUNTIME_LIB_PATTERN.test(entry.name)
|
|
41
|
+
);
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function collectOnnxRuntimeLibraryDirs(): string[] {
|
|
48
|
+
const candidates = new Set<string>();
|
|
49
|
+
|
|
50
|
+
if (process.env.KEYSTONE_ONNX_RUNTIME_LIB_DIR) {
|
|
51
|
+
candidates.add(process.env.KEYSTONE_ONNX_RUNTIME_LIB_DIR);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const runtimeDir = getRuntimeDir();
|
|
55
|
+
const runtimeOnnxDir = join(
|
|
56
|
+
runtimeDir,
|
|
57
|
+
'node_modules',
|
|
58
|
+
'onnxruntime-node',
|
|
59
|
+
'bin',
|
|
60
|
+
'napi-v3',
|
|
61
|
+
process.platform,
|
|
62
|
+
process.arch
|
|
63
|
+
);
|
|
64
|
+
if (existsSync(runtimeOnnxDir)) {
|
|
65
|
+
candidates.add(runtimeOnnxDir);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const nodeModulesDir = join(
|
|
69
|
+
process.cwd(),
|
|
70
|
+
'node_modules',
|
|
71
|
+
'onnxruntime-node',
|
|
72
|
+
'bin',
|
|
73
|
+
'napi-v3',
|
|
74
|
+
process.platform,
|
|
75
|
+
process.arch
|
|
76
|
+
);
|
|
77
|
+
if (existsSync(nodeModulesDir)) {
|
|
78
|
+
candidates.add(nodeModulesDir);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const execDir = dirname(process.execPath);
|
|
82
|
+
candidates.add(execDir);
|
|
83
|
+
candidates.add(join(execDir, 'lib'));
|
|
84
|
+
|
|
85
|
+
return Array.from(candidates).filter(hasOnnxRuntimeLibrary);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function findOnnxRuntimeLibraryPath(dirs: string[]): string | null {
|
|
89
|
+
for (const dir of dirs) {
|
|
90
|
+
try {
|
|
91
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
92
|
+
if (entry.isFile() && ONNX_RUNTIME_LIB_PATTERN.test(entry.name)) {
|
|
93
|
+
return join(dir, entry.name);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
// Ignore unreadable directories.
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function ensureOnnxRuntimeLibraryPath(): void {
|
|
104
|
+
const libDirs = collectOnnxRuntimeLibraryDirs();
|
|
105
|
+
if (!libDirs.length) return;
|
|
106
|
+
|
|
107
|
+
const runtimePath = findOnnxRuntimeLibraryPath(libDirs);
|
|
108
|
+
if (runtimePath) {
|
|
109
|
+
const tempDirs = process.platform === 'darwin' ? ['/private/tmp', '/tmp'] : ['/tmp'];
|
|
110
|
+
for (const tempDir of tempDirs) {
|
|
111
|
+
try {
|
|
112
|
+
const target = join(tempDir, basename(runtimePath));
|
|
113
|
+
if (!existsSync(target)) {
|
|
114
|
+
copyFileSync(runtimePath, target);
|
|
115
|
+
}
|
|
116
|
+
} catch {
|
|
117
|
+
// Best-effort copy for runtimes that extract native modules into temp.
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const envKey =
|
|
123
|
+
process.platform === 'darwin'
|
|
124
|
+
? 'DYLD_LIBRARY_PATH'
|
|
125
|
+
: process.platform === 'win32'
|
|
126
|
+
? 'PATH'
|
|
127
|
+
: 'LD_LIBRARY_PATH';
|
|
128
|
+
const delimiter = process.platform === 'win32' ? ';' : ':';
|
|
129
|
+
const existing = (process.env[envKey] || '').split(delimiter).filter(Boolean);
|
|
130
|
+
const merged: string[] = [];
|
|
131
|
+
const seen = new Set<string>();
|
|
132
|
+
|
|
133
|
+
for (const dir of [...libDirs, ...existing]) {
|
|
134
|
+
if (seen.has(dir)) continue;
|
|
135
|
+
seen.add(dir);
|
|
136
|
+
merged.push(dir);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
process.env[envKey] = merged.join(delimiter);
|
|
140
|
+
if (runtimePath && typeof Bun !== 'undefined' && typeof Bun.dlopen === 'function') {
|
|
141
|
+
try {
|
|
142
|
+
Bun.dlopen(runtimePath, {});
|
|
143
|
+
} catch {
|
|
144
|
+
// Best-effort preloading for compiled binaries.
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function resolveNativeModuleFallback(request: string, parentFilename: string): string | null {
|
|
150
|
+
const normalizedRequest = request.replace(/\\/g, '/');
|
|
151
|
+
const fileName = normalizedRequest.split('/').pop();
|
|
152
|
+
if (!fileName) return null;
|
|
153
|
+
|
|
154
|
+
if (fileName.startsWith('sharp-') || /[\\/]sharp[\\/]/.test(parentFilename)) {
|
|
155
|
+
const candidate = join(getRuntimeDir(), 'node_modules', 'sharp', 'build', 'Release', fileName);
|
|
156
|
+
if (existsSync(candidate)) {
|
|
157
|
+
return candidate;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (
|
|
162
|
+
fileName === 'onnxruntime_binding.node' ||
|
|
163
|
+
/[\\/]onnxruntime-node[\\/]/.test(parentFilename)
|
|
164
|
+
) {
|
|
165
|
+
const candidate = join(
|
|
166
|
+
getRuntimeDir(),
|
|
167
|
+
'node_modules',
|
|
168
|
+
'onnxruntime-node',
|
|
169
|
+
'bin',
|
|
170
|
+
'napi-v3',
|
|
171
|
+
process.platform,
|
|
172
|
+
process.arch,
|
|
173
|
+
'onnxruntime_binding.node'
|
|
174
|
+
);
|
|
175
|
+
if (existsSync(candidate)) {
|
|
176
|
+
return candidate;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function ensureNativeModuleFallbacks(): void {
|
|
184
|
+
if (nativeFallbacksRegistered) return;
|
|
185
|
+
nativeFallbacksRegistered = true;
|
|
186
|
+
|
|
187
|
+
const moduleAny = Module as unknown as {
|
|
188
|
+
_resolveFilename: (
|
|
189
|
+
request: string,
|
|
190
|
+
parent?: { filename?: string },
|
|
191
|
+
isMain?: boolean,
|
|
192
|
+
options?: unknown
|
|
193
|
+
) => string;
|
|
194
|
+
};
|
|
195
|
+
const originalResolve = moduleAny._resolveFilename;
|
|
196
|
+
if (typeof originalResolve !== 'function') return;
|
|
197
|
+
|
|
198
|
+
moduleAny._resolveFilename = function resolveFilename(request, parent, isMain, options) {
|
|
199
|
+
if (typeof request === 'string' && request.endsWith('.node')) {
|
|
200
|
+
try {
|
|
201
|
+
return originalResolve.call(this, request, parent, isMain, options);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
const parentFilename = parent && typeof parent.filename === 'string' ? parent.filename : '';
|
|
204
|
+
const fallback = resolveNativeModuleFallback(request, parentFilename);
|
|
205
|
+
if (fallback) {
|
|
206
|
+
return fallback;
|
|
207
|
+
}
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return originalResolve.call(this, request, parent, isMain, options);
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function resolveTransformersCacheDir(): string | null {
|
|
216
|
+
if (process.env.TRANSFORMERS_CACHE) {
|
|
217
|
+
return process.env.TRANSFORMERS_CACHE;
|
|
218
|
+
}
|
|
219
|
+
if (process.env.XDG_CACHE_HOME) {
|
|
220
|
+
return join(process.env.XDG_CACHE_HOME, 'keystone', 'transformers');
|
|
221
|
+
}
|
|
222
|
+
const home = process.env.HOME || homedir();
|
|
223
|
+
if (home) {
|
|
224
|
+
return join(home, '.cache', 'keystone', 'transformers');
|
|
225
|
+
}
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function getTransformersPipeline(): Promise<TransformersPipeline> {
|
|
230
|
+
if (!cachedPipeline) {
|
|
231
|
+
ensureNativeModuleFallbacks();
|
|
232
|
+
ensureRuntimeResolver();
|
|
233
|
+
const resolved = resolveTransformersPath();
|
|
234
|
+
const module = resolved
|
|
235
|
+
? await import(pathToFileURL(resolved).href)
|
|
236
|
+
: await import('@xenova/transformers');
|
|
237
|
+
if (module.env?.cacheDir?.includes('/$bunfs')) {
|
|
238
|
+
const cacheDir = resolveTransformersCacheDir();
|
|
239
|
+
if (cacheDir) {
|
|
240
|
+
module.env.cacheDir = cacheDir;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
cachedPipeline = module.pipeline;
|
|
244
|
+
}
|
|
245
|
+
return cachedPipeline;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function resolveTransformersPath(): string | null {
|
|
249
|
+
try {
|
|
250
|
+
if (
|
|
251
|
+
process.env.KEYSTONE_TRANSFORMERS_PATH &&
|
|
252
|
+
existsSync(process.env.KEYSTONE_TRANSFORMERS_PATH)
|
|
253
|
+
) {
|
|
254
|
+
return process.env.KEYSTONE_TRANSFORMERS_PATH;
|
|
255
|
+
}
|
|
256
|
+
} catch {
|
|
257
|
+
// Ignore resolve failures and fall back to bundled module.
|
|
258
|
+
}
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function getRuntimeDir(): string {
|
|
263
|
+
return process.env.KEYSTONE_RUNTIME_DIR || join(dirname(process.execPath), 'keystone-runtime');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function resolveRuntimePackageEntry(pkg: string, entry: string): string | null {
|
|
267
|
+
const runtimePath = join(getRuntimeDir(), 'node_modules', ...pkg.split('/'), entry);
|
|
268
|
+
if (existsSync(runtimePath)) {
|
|
269
|
+
return runtimePath;
|
|
270
|
+
}
|
|
271
|
+
const cwdPath = join(process.cwd(), 'node_modules', ...pkg.split('/'), entry);
|
|
272
|
+
if (existsSync(cwdPath)) {
|
|
273
|
+
return cwdPath;
|
|
274
|
+
}
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function ensureRuntimeResolver(): void {
|
|
279
|
+
if (runtimeResolverRegistered) return;
|
|
280
|
+
if (typeof Bun === 'undefined' || typeof Bun.plugin !== 'function') {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const entryMap: Record<string, string> = {
|
|
285
|
+
'@huggingface/jinja': 'dist/index.js',
|
|
286
|
+
sharp: 'lib/index.js',
|
|
287
|
+
'onnxruntime-node': 'dist/index.js',
|
|
288
|
+
'onnxruntime-common': 'dist/ort-common.node.js',
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
Bun.plugin({
|
|
292
|
+
name: 'keystone-runtime-resolver',
|
|
293
|
+
setup(builder) {
|
|
294
|
+
builder.onResolve(
|
|
295
|
+
{ filter: /^(sharp|onnxruntime-node|onnxruntime-common|@huggingface\/jinja)$/ },
|
|
296
|
+
(args) => {
|
|
297
|
+
const entry = entryMap[args.path];
|
|
298
|
+
if (!entry) return null;
|
|
299
|
+
const resolved = resolveRuntimePackageEntry(args.path, entry);
|
|
300
|
+
if (!resolved) return null;
|
|
301
|
+
return { path: resolved };
|
|
302
|
+
}
|
|
303
|
+
);
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
runtimeResolverRegistered = true;
|
|
308
|
+
}
|
|
25
309
|
|
|
26
310
|
export interface LLMMessage {
|
|
27
311
|
role: 'system' | 'user' | 'assistant' | 'tool';
|
|
@@ -1054,7 +1338,16 @@ export class LocalEmbeddingAdapter implements LLMAdapter {
|
|
|
1054
1338
|
async embed(text: string, model = 'Xenova/all-MiniLM-L6-v2'): Promise<number[]> {
|
|
1055
1339
|
const modelToUse = model === 'local' ? 'Xenova/all-MiniLM-L6-v2' : model;
|
|
1056
1340
|
if (!LocalEmbeddingAdapter.extractor) {
|
|
1057
|
-
|
|
1341
|
+
try {
|
|
1342
|
+
ensureOnnxRuntimeLibraryPath();
|
|
1343
|
+
const pipeline = await getTransformersPipeline();
|
|
1344
|
+
LocalEmbeddingAdapter.extractor = await pipeline('feature-extraction', modelToUse);
|
|
1345
|
+
} catch (error) {
|
|
1346
|
+
const details = error instanceof Error ? error.message : String(error);
|
|
1347
|
+
throw new Error(
|
|
1348
|
+
`Failed to initialize local embeddings. If you are running a compiled binary, ensure the keystone-runtime directory is next to the executable (or set KEYSTONE_RUNTIME_DIR), and that the ONNX Runtime shared library is available (set KEYSTONE_ONNX_RUNTIME_LIB_DIR or place it next to the executable). Original error: ${details}`
|
|
1349
|
+
);
|
|
1350
|
+
}
|
|
1058
1351
|
}
|
|
1059
1352
|
const output = await LocalEmbeddingAdapter.extractor(text, {
|
|
1060
1353
|
pooling: 'mean',
|
|
@@ -152,9 +152,9 @@ steps:
|
|
|
152
152
|
|
|
153
153
|
try {
|
|
154
154
|
// The implementation skips directories that don't exist or can't be read
|
|
155
|
-
// This is expected behavior - return
|
|
155
|
+
// This is expected behavior - return a list without throwing
|
|
156
156
|
const workflows = WorkflowRegistry.listWorkflows();
|
|
157
|
-
expect(workflows).
|
|
157
|
+
expect(Array.isArray(workflows)).toBe(true);
|
|
158
158
|
} finally {
|
|
159
159
|
homedirSpy.mockRestore();
|
|
160
160
|
cwdSpy.mockRestore();
|