orionfold-relay 0.15.0 → 0.15.2
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 +0 -14
- package/dist/cli.js +69 -34
- package/package.json +4 -4
- package/src/app/api/projects/route.ts +2 -0
- package/src/app/projects/page.tsx +1 -0
- package/src/components/projects/project-form-sheet.tsx +54 -1
- package/src/components/projects/project-list.tsx +1 -0
- package/src/instrumentation-node.ts +16 -0
- package/src/lib/chat/engine.ts +4 -1
- package/src/lib/packs/install.ts +21 -7
- package/src/lib/utils/app-root.ts +44 -3
- package/src/lib/utils/migrate-mcp-namespace.ts +94 -0
- package/src/lib/validators/project.ts +4 -0
package/README.md
CHANGED
|
@@ -194,20 +194,6 @@ See `features/roadmap.md` for the full inventory and `features/stats/snapshot.js
|
|
|
194
194
|
|
|
195
195
|
---
|
|
196
196
|
|
|
197
|
-
## About the author
|
|
198
|
-
|
|
199
|
-
### Manav Sehgal
|
|
200
|
-
|
|
201
|
-
Solutions Leader, AWS Frontier AI. Author, *AI Native Business* open book. 2M+ Kaggle views. Ex Amazon AGI.
|
|
202
|
-
|
|
203
|
-
Orionfold Relay is a personal research project exploring what an AI-native operating system looks like — built over weekends, on a personal laptop, with capped AI plans. The *AI Native Business* open book is its 14-chapter playbook for building autonomous business systems with AI agents.
|
|
204
|
-
|
|
205
|
-
Manav is a solutions leader at AWS Frontier AI, collaborating with Anthropic, NVIDIA, and Disney on production AI and agentic systems. His 25-year arc spans Xerox PARC (1996), HCL's digital practice, Daily Mail, Amazon AGI, and AWS, where he has delivered scale AI programs for Amazon Retail and Alexa. He led the AWS pandemic response honored by a President of India award for the customer. He holds credentials from Harvard (Disruptive Strategy), MIT Sloan (Design Thinking), and Berkeley Haas (Leading Innovative Change).
|
|
206
|
-
|
|
207
|
-
> Orionfold Relay and *AI Native Business* are personal works created on Manav's own time and resources. While they may refer to AWS technologies, the opinions expressed are the author's personal opinions and not those of the employer.
|
|
208
|
-
|
|
209
|
-
---
|
|
210
|
-
|
|
211
197
|
## License
|
|
212
198
|
|
|
213
199
|
Licensed under the [Apache License 2.0](LICENSE).
|
package/dist/cli.js
CHANGED
|
@@ -27,18 +27,37 @@ var __copyProps = (to, from, except, desc16) => {
|
|
|
27
27
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
28
|
|
|
29
29
|
// src/lib/utils/app-root.ts
|
|
30
|
-
import { existsSync as existsSync2 } from "fs";
|
|
31
|
-
import { join as join2 } from "path";
|
|
30
|
+
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
31
|
+
import { join as join2, dirname as dirname2 } from "path";
|
|
32
32
|
function getAppRoot(metaDirname, depth) {
|
|
33
33
|
if (metaDirname) {
|
|
34
34
|
const candidate = join2(metaDirname, ...Array(depth).fill(".."));
|
|
35
|
-
if (
|
|
35
|
+
if (isPackageRoot(candidate)) return candidate;
|
|
36
|
+
let dir = metaDirname;
|
|
37
|
+
while (true) {
|
|
38
|
+
if (isPackageRoot(dir)) return dir;
|
|
39
|
+
const parent = dirname2(dir);
|
|
40
|
+
if (parent === dir) break;
|
|
41
|
+
dir = parent;
|
|
42
|
+
}
|
|
36
43
|
}
|
|
37
44
|
return process.cwd();
|
|
38
45
|
}
|
|
46
|
+
function isPackageRoot(dir) {
|
|
47
|
+
const pkgPath = join2(dir, "package.json");
|
|
48
|
+
if (!existsSync2(pkgPath)) return false;
|
|
49
|
+
try {
|
|
50
|
+
const pkg2 = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
51
|
+
return pkg2.name === PKG_NAME;
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
var PKG_NAME;
|
|
39
57
|
var init_app_root = __esm({
|
|
40
58
|
"src/lib/utils/app-root.ts"() {
|
|
41
59
|
"use strict";
|
|
60
|
+
PKG_NAME = "orionfold-relay";
|
|
42
61
|
}
|
|
43
62
|
});
|
|
44
63
|
|
|
@@ -1203,7 +1222,7 @@ var init_types = __esm({
|
|
|
1203
1222
|
|
|
1204
1223
|
// src/lib/environment/parsers/utils.ts
|
|
1205
1224
|
import { createHash } from "crypto";
|
|
1206
|
-
import { readFileSync as
|
|
1225
|
+
import { readFileSync as readFileSync3, statSync } from "fs";
|
|
1207
1226
|
function computeHash(content) {
|
|
1208
1227
|
const truncated = content.slice(0, MAX_HASH_BYTES);
|
|
1209
1228
|
return createHash("sha256").update(truncated).digest("hex");
|
|
@@ -1220,7 +1239,7 @@ function safeStat(path19) {
|
|
|
1220
1239
|
}
|
|
1221
1240
|
function safeReadFile(path19) {
|
|
1222
1241
|
try {
|
|
1223
|
-
return
|
|
1242
|
+
return readFileSync3(path19, "utf-8");
|
|
1224
1243
|
} catch {
|
|
1225
1244
|
return null;
|
|
1226
1245
|
}
|
|
@@ -5744,7 +5763,7 @@ __export(crypto_exports, {
|
|
|
5744
5763
|
getOrCreateKeyfile: () => getOrCreateKeyfile
|
|
5745
5764
|
});
|
|
5746
5765
|
import { randomBytes, createCipheriv, createDecipheriv } from "crypto";
|
|
5747
|
-
import { readFileSync as
|
|
5766
|
+
import { readFileSync as readFileSync4, writeFileSync, existsSync as existsSync5, mkdirSync as mkdirSync2, chmodSync } from "fs";
|
|
5748
5767
|
import { join as join7 } from "path";
|
|
5749
5768
|
function getKeyfilePath() {
|
|
5750
5769
|
return join7(dataDir(), ".keyfile");
|
|
@@ -5752,7 +5771,7 @@ function getKeyfilePath() {
|
|
|
5752
5771
|
function getOrCreateKeyfile() {
|
|
5753
5772
|
const keyfilePath = getKeyfilePath();
|
|
5754
5773
|
if (existsSync5(keyfilePath)) {
|
|
5755
|
-
const key2 =
|
|
5774
|
+
const key2 = readFileSync4(keyfilePath);
|
|
5756
5775
|
if (key2.length !== KEY_LENGTH) {
|
|
5757
5776
|
throw new Error(`Invalid keyfile: expected ${KEY_LENGTH} bytes, got ${key2.length}`);
|
|
5758
5777
|
}
|
|
@@ -6718,7 +6737,7 @@ __export(workspace_context_exports, {
|
|
|
6718
6737
|
getLaunchCwd: () => getLaunchCwd,
|
|
6719
6738
|
getWorkspaceContext: () => getWorkspaceContext
|
|
6720
6739
|
});
|
|
6721
|
-
import { basename, dirname as
|
|
6740
|
+
import { basename, dirname as dirname3 } from "path";
|
|
6722
6741
|
import { homedir as homedir4 } from "os";
|
|
6723
6742
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
6724
6743
|
import { statSync as statSync2 } from "fs";
|
|
@@ -6730,7 +6749,7 @@ function getWorkspaceContext() {
|
|
|
6730
6749
|
const cwd = getLaunchCwd();
|
|
6731
6750
|
const home = homedir4();
|
|
6732
6751
|
const folderName = basename(cwd);
|
|
6733
|
-
const parent =
|
|
6752
|
+
const parent = dirname3(cwd);
|
|
6734
6753
|
const parentPath = parent.startsWith(home) ? "~" + parent.slice(home.length) : parent;
|
|
6735
6754
|
let gitBranch = null;
|
|
6736
6755
|
try {
|
|
@@ -12811,7 +12830,7 @@ var list_fused_profiles_exports = {};
|
|
|
12811
12830
|
__export(list_fused_profiles_exports, {
|
|
12812
12831
|
listFusedProfiles: () => listFusedProfiles
|
|
12813
12832
|
});
|
|
12814
|
-
import { readdirSync, readFileSync as
|
|
12833
|
+
import { readdirSync, readFileSync as readFileSync5, statSync as statSync3, existsSync as existsSync6 } from "fs";
|
|
12815
12834
|
import { join as join11 } from "path";
|
|
12816
12835
|
import { homedir as homedir5 } from "os";
|
|
12817
12836
|
function parseFrontmatter2(content) {
|
|
@@ -12836,7 +12855,7 @@ function loadFilesystemSkills(skillsDir, origin, projectRootDir) {
|
|
|
12836
12855
|
if (!statSync3(skillPath).isDirectory()) continue;
|
|
12837
12856
|
const skillMdPath = join11(skillPath, "SKILL.md");
|
|
12838
12857
|
if (!existsSync6(skillMdPath)) continue;
|
|
12839
|
-
const content =
|
|
12858
|
+
const content = readFileSync5(skillMdPath, "utf8");
|
|
12840
12859
|
const fm = parseFrontmatter2(content);
|
|
12841
12860
|
if (!fm || !fm.name) {
|
|
12842
12861
|
console.warn(
|
|
@@ -15552,7 +15571,7 @@ var init_active_skills = __esm({
|
|
|
15552
15571
|
});
|
|
15553
15572
|
|
|
15554
15573
|
// src/lib/environment/parsers/skill.ts
|
|
15555
|
-
import { readdirSync as readdirSync2, readFileSync as
|
|
15574
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
|
|
15556
15575
|
import { join as join12, basename as basename3 } from "path";
|
|
15557
15576
|
function parseSkillDir(dirPath, tool, scope, baseDir) {
|
|
15558
15577
|
const stat2 = safeStat(dirPath);
|
|
@@ -15566,7 +15585,7 @@ function parseSkillDir(dirPath, tool, scope, baseDir) {
|
|
|
15566
15585
|
const skillFile = files.find((f) => f === "SKILL.md") || files.find((f) => f.endsWith(".md")) || files[0];
|
|
15567
15586
|
if (skillFile) {
|
|
15568
15587
|
mainFile = join12(dirPath, skillFile);
|
|
15569
|
-
content =
|
|
15588
|
+
content = readFileSync6(mainFile, "utf-8");
|
|
15570
15589
|
}
|
|
15571
15590
|
} catch {
|
|
15572
15591
|
return null;
|
|
@@ -16202,7 +16221,7 @@ __export(list_skills_exports, {
|
|
|
16202
16221
|
listSkills: () => listSkills,
|
|
16203
16222
|
listSkillsEnriched: () => listSkillsEnriched
|
|
16204
16223
|
});
|
|
16205
|
-
import { readFileSync as
|
|
16224
|
+
import { readFileSync as readFileSync7, readdirSync as readdirSync6, statSync as statSync4 } from "fs";
|
|
16206
16225
|
import { join as join17 } from "path";
|
|
16207
16226
|
function listSkills(options = {}) {
|
|
16208
16227
|
const projectDir = options.projectDir ?? getLaunchCwd();
|
|
@@ -16227,7 +16246,7 @@ function getSkill(id, options = {}) {
|
|
|
16227
16246
|
const filePath = resolveSkillFile(hit.absPath);
|
|
16228
16247
|
if (!filePath) return null;
|
|
16229
16248
|
try {
|
|
16230
|
-
const content =
|
|
16249
|
+
const content = readFileSync7(filePath, "utf8");
|
|
16231
16250
|
return { ...hit, content };
|
|
16232
16251
|
} catch {
|
|
16233
16252
|
return null;
|
|
@@ -19151,7 +19170,7 @@ var init_codex_app_server_client = __esm({
|
|
|
19151
19170
|
|
|
19152
19171
|
// src/lib/agents/runtime/openai-codex-auth.ts
|
|
19153
19172
|
import { mkdir as mkdir2, readFile as readFile7, rm, writeFile } from "fs/promises";
|
|
19154
|
-
import { dirname as
|
|
19173
|
+
import { dirname as dirname4 } from "path";
|
|
19155
19174
|
function parseRateLimitWindow(value) {
|
|
19156
19175
|
if (!value || typeof value !== "object") return null;
|
|
19157
19176
|
return {
|
|
@@ -19211,7 +19230,7 @@ async function ensureCodexHomeConfig() {
|
|
|
19211
19230
|
const codexDir = getAinativeCodexDir();
|
|
19212
19231
|
const configPath = getAinativeCodexConfigPath();
|
|
19213
19232
|
await mkdir2(codexDir, { recursive: true });
|
|
19214
|
-
await mkdir2(
|
|
19233
|
+
await mkdir2(dirname4(configPath), { recursive: true });
|
|
19215
19234
|
let current = "";
|
|
19216
19235
|
try {
|
|
19217
19236
|
current = await readFile7(configPath, "utf8");
|
|
@@ -25183,6 +25202,9 @@ import { execFileSync as execFileSync3 } from "child_process";
|
|
|
25183
25202
|
import yaml12 from "js-yaml";
|
|
25184
25203
|
import semver from "semver";
|
|
25185
25204
|
function relayCoreVersion() {
|
|
25205
|
+
if (semver.valid("0.15.2")) {
|
|
25206
|
+
return "0.15.2";
|
|
25207
|
+
}
|
|
25186
25208
|
try {
|
|
25187
25209
|
const root = getAppRoot(import.meta.dirname, 3);
|
|
25188
25210
|
const pkg2 = JSON.parse(
|
|
@@ -25551,13 +25573,13 @@ var init_cli = __esm({
|
|
|
25551
25573
|
|
|
25552
25574
|
// bin/cli.ts
|
|
25553
25575
|
import { program } from "commander";
|
|
25554
|
-
import { basename as basename5, dirname as
|
|
25576
|
+
import { basename as basename5, dirname as dirname5, join as join20 } from "path";
|
|
25555
25577
|
import { homedir as homedir8 } from "os";
|
|
25556
25578
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
25557
25579
|
import {
|
|
25558
25580
|
mkdirSync as mkdirSync5,
|
|
25559
25581
|
existsSync as existsSync12,
|
|
25560
|
-
readFileSync as
|
|
25582
|
+
readFileSync as readFileSync8,
|
|
25561
25583
|
writeFileSync as writeFileSync5,
|
|
25562
25584
|
cpSync as cpSync2,
|
|
25563
25585
|
unlinkSync as unlinkSync2
|
|
@@ -25629,14 +25651,14 @@ init_ainative_paths();
|
|
|
25629
25651
|
init_bootstrap();
|
|
25630
25652
|
|
|
25631
25653
|
// src/lib/utils/migrate-to-ainative.ts
|
|
25632
|
-
import { existsSync as existsSync3, renameSync, cpSync, rmSync, readFileSync } from "fs";
|
|
25654
|
+
import { existsSync as existsSync3, renameSync, cpSync, rmSync, readFileSync as readFileSync2 } from "fs";
|
|
25633
25655
|
import { join as join5 } from "path";
|
|
25634
25656
|
import { homedir as homedir2 } from "os";
|
|
25635
25657
|
import Database from "better-sqlite3";
|
|
25636
25658
|
function hasSqliteHeader(path19) {
|
|
25637
25659
|
const SQLITE_MAGIC = "SQLite format 3\0";
|
|
25638
25660
|
try {
|
|
25639
|
-
const header =
|
|
25661
|
+
const header = readFileSync2(path19, { encoding: null });
|
|
25640
25662
|
return header.length >= 16 && header.subarray(0, 16).toString("binary") === SQLITE_MAGIC;
|
|
25641
25663
|
} catch {
|
|
25642
25664
|
return false;
|
|
@@ -25758,7 +25780,7 @@ async function migrateLegacyData(options = {}) {
|
|
|
25758
25780
|
|
|
25759
25781
|
// bin/cli.ts
|
|
25760
25782
|
init_detect();
|
|
25761
|
-
var __dirname =
|
|
25783
|
+
var __dirname = dirname5(fileURLToPath3(import.meta.url));
|
|
25762
25784
|
var appDir = join20(__dirname, "..");
|
|
25763
25785
|
var launchCwd2 = process.cwd();
|
|
25764
25786
|
var _envLocalPath = join20(launchCwd2, ".env.local");
|
|
@@ -25766,18 +25788,31 @@ var _firstRunNeedsEnv = !existsSync12(_envLocalPath) && !process.env.RELAY_DATA_
|
|
|
25766
25788
|
if (_firstRunNeedsEnv) {
|
|
25767
25789
|
const folderName = basename5(launchCwd2);
|
|
25768
25790
|
const autoDataDir = join20(homedir8(), `.${folderName}`);
|
|
25769
|
-
|
|
25770
|
-
|
|
25771
|
-
|
|
25791
|
+
try {
|
|
25792
|
+
writeFileSync5(
|
|
25793
|
+
_envLocalPath,
|
|
25794
|
+
`# Auto-created by orionfold-relay on first run.
|
|
25772
25795
|
# Points this folder's install at an isolated data directory.
|
|
25773
25796
|
RELAY_DATA_DIR=${autoDataDir}
|
|
25774
25797
|
`,
|
|
25775
|
-
|
|
25776
|
-
|
|
25777
|
-
|
|
25798
|
+
"utf-8"
|
|
25799
|
+
);
|
|
25800
|
+
console.log(`First run \u2014 wrote ${_envLocalPath} (RELAY_DATA_DIR=${autoDataDir}).`);
|
|
25801
|
+
} catch (e) {
|
|
25802
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
25803
|
+
console.warn(
|
|
25804
|
+
`Warning: could not write ${_envLocalPath} (${reason}).
|
|
25805
|
+
Continuing with the default data directory (~/.relay). This folder will not get an isolated database.`
|
|
25806
|
+
);
|
|
25807
|
+
if (/^([A-Za-z]:)?[\\/]Windows([\\/]|$)/.test(launchCwd2)) {
|
|
25808
|
+
console.warn(
|
|
25809
|
+
`It looks like you launched from a Windows UNC path under WSL, so the working directory defaulted to "${launchCwd2}". Run relay from your Linux filesystem instead \u2014 e.g. \`cd ~\` first, or run it from a WSL home directory rather than a \\\\wsl.localhost\\... path.`
|
|
25810
|
+
);
|
|
25811
|
+
}
|
|
25812
|
+
}
|
|
25778
25813
|
}
|
|
25779
25814
|
if (existsSync12(_envLocalPath)) {
|
|
25780
|
-
for (const line of
|
|
25815
|
+
for (const line of readFileSync8(_envLocalPath, "utf-8").split("\n")) {
|
|
25781
25816
|
const trimmed = line.trim();
|
|
25782
25817
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
25783
25818
|
const eqIdx = trimmed.indexOf("=");
|
|
@@ -25789,7 +25824,7 @@ if (existsSync12(_envLocalPath)) {
|
|
|
25789
25824
|
}
|
|
25790
25825
|
}
|
|
25791
25826
|
}
|
|
25792
|
-
var pkg = JSON.parse(
|
|
25827
|
+
var pkg = JSON.parse(readFileSync8(join20(appDir, "package.json"), "utf-8"));
|
|
25793
25828
|
function getHelpText() {
|
|
25794
25829
|
const dir = getAinativeDataDir();
|
|
25795
25830
|
const db3 = getAinativeDbPath();
|
|
@@ -25910,8 +25945,8 @@ async function main() {
|
|
|
25910
25945
|
let effectiveCwd = appDir;
|
|
25911
25946
|
const localNm = join20(appDir, "node_modules");
|
|
25912
25947
|
if (!existsSync12(join20(localNm, "next", "package.json"))) {
|
|
25913
|
-
let searchDir =
|
|
25914
|
-
while (searchDir !==
|
|
25948
|
+
let searchDir = dirname5(appDir);
|
|
25949
|
+
while (searchDir !== dirname5(searchDir)) {
|
|
25915
25950
|
const candidate = join20(searchDir, "node_modules", "next", "package.json");
|
|
25916
25951
|
if (existsSync12(candidate)) {
|
|
25917
25952
|
const hoistedRoot = searchDir;
|
|
@@ -25933,13 +25968,13 @@ async function main() {
|
|
|
25933
25968
|
const dest = join20(hoistedRoot, name);
|
|
25934
25969
|
const src = join20(appDir, name);
|
|
25935
25970
|
if (!existsSync12(dest) && existsSync12(src)) {
|
|
25936
|
-
writeFileSync5(dest,
|
|
25971
|
+
writeFileSync5(dest, readFileSync8(src));
|
|
25937
25972
|
}
|
|
25938
25973
|
}
|
|
25939
25974
|
effectiveCwd = hoistedRoot;
|
|
25940
25975
|
break;
|
|
25941
25976
|
}
|
|
25942
|
-
searchDir =
|
|
25977
|
+
searchDir = dirname5(searchDir);
|
|
25943
25978
|
}
|
|
25944
25979
|
}
|
|
25945
25980
|
const nextEntrypoint = resolveNextEntrypoint(effectiveCwd);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orionfold-relay",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.2",
|
|
4
4
|
"description": "Orionfold Relay — a local-first, multi-agent orchestration runtime and builder scaffold for AI-native work.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -52,12 +52,12 @@
|
|
|
52
52
|
],
|
|
53
53
|
"repository": {
|
|
54
54
|
"type": "git",
|
|
55
|
-
"url": "https://github.com/
|
|
55
|
+
"url": "https://github.com/orionfold/relay.git"
|
|
56
56
|
},
|
|
57
57
|
"bugs": {
|
|
58
|
-
"url": "https://github.com/
|
|
58
|
+
"url": "https://github.com/orionfold/relay/issues"
|
|
59
59
|
},
|
|
60
|
-
"homepage": "https://
|
|
60
|
+
"homepage": "https://orionfold.com/relay/",
|
|
61
61
|
"scripts": {
|
|
62
62
|
"dev": "next dev --turbopack",
|
|
63
63
|
"build": "next build",
|
|
@@ -13,6 +13,7 @@ export async function GET() {
|
|
|
13
13
|
name: projects.name,
|
|
14
14
|
description: projects.description,
|
|
15
15
|
workingDirectory: projects.workingDirectory,
|
|
16
|
+
customerId: projects.customerId,
|
|
16
17
|
status: projects.status,
|
|
17
18
|
createdAt: projects.createdAt,
|
|
18
19
|
updatedAt: projects.updatedAt,
|
|
@@ -42,6 +43,7 @@ export async function POST(req: NextRequest) {
|
|
|
42
43
|
name: parsed.data.name,
|
|
43
44
|
description: parsed.data.description ?? null,
|
|
44
45
|
workingDirectory: parsed.data.workingDirectory ?? null,
|
|
46
|
+
customerId: parsed.data.customerId ?? null,
|
|
45
47
|
status: "active",
|
|
46
48
|
createdAt: now,
|
|
47
49
|
updatedAt: now,
|
|
@@ -13,6 +13,7 @@ export default async function ProjectsPage() {
|
|
|
13
13
|
name: projects.name,
|
|
14
14
|
description: projects.description,
|
|
15
15
|
workingDirectory: projects.workingDirectory,
|
|
16
|
+
customerId: projects.customerId,
|
|
16
17
|
status: projects.status,
|
|
17
18
|
createdAt: projects.createdAt,
|
|
18
19
|
updatedAt: projects.updatedAt,
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
SelectTrigger,
|
|
20
20
|
SelectValue,
|
|
21
21
|
} from "@/components/ui/select";
|
|
22
|
-
import { FolderOpen, AlignLeft, FolderCode, Trash2, Paperclip, Plus, X } from "lucide-react";
|
|
22
|
+
import { FolderOpen, AlignLeft, FolderCode, Trash2, Paperclip, Plus, X, Building2 } from "lucide-react";
|
|
23
23
|
import { toast } from "sonner";
|
|
24
24
|
import { Badge } from "@/components/ui/badge";
|
|
25
25
|
import { ConfirmDialog } from "@/components/shared/confirm-dialog";
|
|
@@ -31,9 +31,18 @@ interface Project {
|
|
|
31
31
|
name: string;
|
|
32
32
|
description: string | null;
|
|
33
33
|
workingDirectory: string | null;
|
|
34
|
+
customerId: string | null;
|
|
34
35
|
status: string;
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
interface CustomerOption {
|
|
39
|
+
id: string;
|
|
40
|
+
name: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Radix Select cannot use an empty-string value; this sentinel maps to null.
|
|
44
|
+
const NO_CUSTOMER = "__none__";
|
|
45
|
+
|
|
37
46
|
interface ProjectFormSheetProps {
|
|
38
47
|
mode: "create" | "edit";
|
|
39
48
|
project?: Project | null;
|
|
@@ -53,6 +62,8 @@ export function ProjectFormSheet({
|
|
|
53
62
|
const [description, setDescription] = useState("");
|
|
54
63
|
const [workingDirectory, setWorkingDirectory] = useState("");
|
|
55
64
|
const [status, setStatus] = useState("active");
|
|
65
|
+
const [customerId, setCustomerId] = useState<string>(NO_CUSTOMER);
|
|
66
|
+
const [customers, setCustomers] = useState<CustomerOption[]>([]);
|
|
56
67
|
const [loading, setLoading] = useState(false);
|
|
57
68
|
const [error, setError] = useState<string | null>(null);
|
|
58
69
|
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
@@ -60,6 +71,17 @@ export function ProjectFormSheet({
|
|
|
60
71
|
const [selectedDocs, setSelectedDocs] = useState<Array<{ id: string; originalName: string; mimeType: string; size: number }>>([]);
|
|
61
72
|
const [pickerOpen, setPickerOpen] = useState(false);
|
|
62
73
|
|
|
74
|
+
// Load selectable customers whenever the sheet opens
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (!open) return;
|
|
77
|
+
fetch("/api/customers")
|
|
78
|
+
.then((r) => r.json())
|
|
79
|
+
.then((rows: Array<{ id: string; name: string }>) => {
|
|
80
|
+
setCustomers(rows.map((c) => ({ id: c.id, name: c.name })));
|
|
81
|
+
})
|
|
82
|
+
.catch(() => setCustomers([]));
|
|
83
|
+
}, [open]);
|
|
84
|
+
|
|
63
85
|
// Pre-fill form in edit mode
|
|
64
86
|
useEffect(() => {
|
|
65
87
|
if (mode === "edit" && project) {
|
|
@@ -67,6 +89,7 @@ export function ProjectFormSheet({
|
|
|
67
89
|
setDescription(project.description ?? "");
|
|
68
90
|
setWorkingDirectory(project.workingDirectory ?? "");
|
|
69
91
|
setStatus(project.status);
|
|
92
|
+
setCustomerId(project.customerId ?? NO_CUSTOMER);
|
|
70
93
|
// Load existing default documents
|
|
71
94
|
fetch(`/api/projects/${project.id}/documents`)
|
|
72
95
|
.then((r) => r.json())
|
|
@@ -91,6 +114,7 @@ export function ProjectFormSheet({
|
|
|
91
114
|
setDescription("");
|
|
92
115
|
setWorkingDirectory("");
|
|
93
116
|
setStatus("active");
|
|
117
|
+
setCustomerId(NO_CUSTOMER);
|
|
94
118
|
setSelectedDocIds(new Set());
|
|
95
119
|
setSelectedDocs([]);
|
|
96
120
|
}
|
|
@@ -120,6 +144,7 @@ export function ProjectFormSheet({
|
|
|
120
144
|
name: name.trim(),
|
|
121
145
|
description: description.trim() || undefined,
|
|
122
146
|
workingDirectory: workingDirectory.trim() || undefined,
|
|
147
|
+
customerId: customerId === NO_CUSTOMER ? null : customerId,
|
|
123
148
|
documentIds: selectedDocIds.size > 0 ? [...selectedDocIds] : undefined,
|
|
124
149
|
}),
|
|
125
150
|
});
|
|
@@ -140,6 +165,7 @@ export function ProjectFormSheet({
|
|
|
140
165
|
description: description.trim() || undefined,
|
|
141
166
|
workingDirectory: workingDirectory.trim() || undefined,
|
|
142
167
|
status,
|
|
168
|
+
customerId: customerId === NO_CUSTOMER ? null : customerId,
|
|
143
169
|
documentIds: [...selectedDocIds],
|
|
144
170
|
}),
|
|
145
171
|
});
|
|
@@ -241,6 +267,33 @@ export function ProjectFormSheet({
|
|
|
241
267
|
</p>
|
|
242
268
|
</div>
|
|
243
269
|
|
|
270
|
+
<div className="space-y-2">
|
|
271
|
+
<Label htmlFor="proj-customer" className="flex items-center gap-1.5">
|
|
272
|
+
<Building2 className="h-3.5 w-3.5 text-muted-foreground" />
|
|
273
|
+
Customer
|
|
274
|
+
</Label>
|
|
275
|
+
<Select value={customerId} onValueChange={setCustomerId}>
|
|
276
|
+
<SelectTrigger id="proj-customer">
|
|
277
|
+
<SelectValue placeholder="No customer" />
|
|
278
|
+
</SelectTrigger>
|
|
279
|
+
<SelectContent>
|
|
280
|
+
<SelectItem value={NO_CUSTOMER}>
|
|
281
|
+
<span className="text-muted-foreground">No customer</span>
|
|
282
|
+
</SelectItem>
|
|
283
|
+
{customers.map((c) => (
|
|
284
|
+
<SelectItem key={c.id} value={c.id}>
|
|
285
|
+
{c.name}
|
|
286
|
+
</SelectItem>
|
|
287
|
+
))}
|
|
288
|
+
</SelectContent>
|
|
289
|
+
</Select>
|
|
290
|
+
<p className="text-xs text-muted-foreground">
|
|
291
|
+
{customers.length === 0
|
|
292
|
+
? "No customers yet — create one to attribute this project's AI spend."
|
|
293
|
+
: "Link to a customer so this project's AI spend rolls up per customer."}
|
|
294
|
+
</p>
|
|
295
|
+
</div>
|
|
296
|
+
|
|
244
297
|
{isEdit && (
|
|
245
298
|
<div className="space-y-2">
|
|
246
299
|
<Label htmlFor="proj-status">Status</Label>
|
|
@@ -3,6 +3,22 @@ export async function registerNodeInstrumentation() {
|
|
|
3
3
|
const { migrateLegacyData } = await import("@/lib/utils/migrate-to-ainative");
|
|
4
4
|
await migrateLegacyData();
|
|
5
5
|
|
|
6
|
+
// ainative→relay hop: the runtime now publishes chat/compose tools as
|
|
7
|
+
// mcp__relay__* (engine.ts server key), so previously-saved allow-lists and
|
|
8
|
+
// "Always Allow" records — matched by exact string — must be rewritten too.
|
|
9
|
+
// Runs against the live DB; idempotent, never throws.
|
|
10
|
+
const { migrateMcpNamespace } = await import("@/lib/utils/migrate-mcp-namespace");
|
|
11
|
+
const { sqlite } = await import("@/lib/db");
|
|
12
|
+
const nsReport = migrateMcpNamespace(sqlite);
|
|
13
|
+
if (nsReport.profilesUpdated > 0 || nsReport.permissionsUpdated > 0) {
|
|
14
|
+
console.log(
|
|
15
|
+
`[migrate] mcp namespace ainative→relay: ${nsReport.profilesUpdated} profile(s), ${nsReport.permissionsUpdated} permission set(s)`,
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
for (const err of nsReport.errors) {
|
|
19
|
+
console.error(`[migrate] mcp namespace: ${err}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
6
22
|
// Instance bootstrap — creates local branch, handles dev-mode gates, consent flow.
|
|
7
23
|
// Runs BEFORE other startup so instance config is available downstream.
|
|
8
24
|
// Safe in the canonical Relay dev repo thanks to RELAY_DEV_MODE=true
|
package/src/lib/chat/engine.ts
CHANGED
|
@@ -505,7 +505,10 @@ export async function* sendMessage(
|
|
|
505
505
|
// Keep only last 50 chunks to avoid unbounded memory
|
|
506
506
|
if (stderrChunks.length > 50) stderrChunks.shift();
|
|
507
507
|
},
|
|
508
|
-
|
|
508
|
+
// Server key = tool namespace: the Agent SDK derives `mcp__<key>__*`
|
|
509
|
+
// from THIS map key. Must be `relay` so published tools match the
|
|
510
|
+
// allow-list (:510) and the auto-allow gate (:533) below.
|
|
511
|
+
mcpServers: { relay: ainativeServer, ...browserServers, ...externalServers },
|
|
509
512
|
allowedTools: [
|
|
510
513
|
"mcp__relay__*",
|
|
511
514
|
...browserToolPatterns,
|
package/src/lib/packs/install.ts
CHANGED
|
@@ -19,14 +19,28 @@ import {
|
|
|
19
19
|
type ResolvedPackFile,
|
|
20
20
|
} from "./format";
|
|
21
21
|
|
|
22
|
-
//
|
|
23
|
-
//
|
|
22
|
+
// Compile-time core version, embedded by tsup's `define` (see tsup.config.ts).
|
|
23
|
+
// Present ONLY in the bundled CLI; `undefined` in dev/test/Next.js builds,
|
|
24
|
+
// where the runtime lookup below takes over. Declared as a global so the
|
|
25
|
+
// reference type-checks in the non-bundled builds where it doesn't exist.
|
|
26
|
+
declare const __RELAY_CORE_VERSION__: string | undefined;
|
|
27
|
+
|
|
28
|
+
// The current relay-core version. Kept here (not inlined) so the compat gate
|
|
29
|
+
// has a single point of truth.
|
|
24
30
|
function relayCoreVersion(): string {
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
|
|
31
|
+
// 1. Bundle path: the version baked in at build time. This eliminates the
|
|
32
|
+
// runtime package.json lookup entirely in the shipped CLI — the source of
|
|
33
|
+
// the "0.0.0" bug, where the flattened dist/ layout broke the depth-based
|
|
34
|
+
// getAppRoot resolution and fell back to the user's launch dir.
|
|
35
|
+
if (typeof __RELAY_CORE_VERSION__ === "string" && semver.valid(__RELAY_CORE_VERSION__)) {
|
|
36
|
+
return __RELAY_CORE_VERSION__;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 2. Dev/test/Next.js path: resolve the app root via getAppRoot (NOT
|
|
40
|
+
// process.cwd() — under npx that is the user's launch dir; see
|
|
41
|
+
// npx-process-cwd.test.ts) and read package.json. getAppRoot is now
|
|
42
|
+
// bundle-aware (walks up to the orionfold-relay package.json), so this
|
|
43
|
+
// path is also correct in the bundle even if the define is ever dropped.
|
|
30
44
|
try {
|
|
31
45
|
const root = getAppRoot(import.meta.dirname, 3);
|
|
32
46
|
const pkg = JSON.parse(
|
|
@@ -1,11 +1,30 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join, dirname } from "node:path";
|
|
3
|
+
|
|
4
|
+
const PKG_NAME = "orionfold-relay";
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* Resolve the app root directory.
|
|
6
8
|
* - import.meta.dirname works under npx (real path to installed package)
|
|
7
9
|
* - Turbopack compiles it to /ROOT/... (virtual, doesn't exist) → fall back to process.cwd()
|
|
8
10
|
*
|
|
11
|
+
* Two layouts must resolve correctly:
|
|
12
|
+
* 1. Source tree — the `depth` a caller passes lands exactly on the repo
|
|
13
|
+
* root (e.g. src/lib/packs/ → depth 3). This is the fast path and stays
|
|
14
|
+
* byte-identical to the original behavior.
|
|
15
|
+
* 2. Bundled dist/cli.js — tsup flattens every module into one file, so
|
|
16
|
+
* `import.meta.dirname` is `dist/` for ALL callers regardless of the
|
|
17
|
+
* depth they pass (a source-tree assumption). Depth then overshoots and
|
|
18
|
+
* the depth candidate misses. Rather than fix depth math at every call
|
|
19
|
+
* site (fragile — 5 sites, different depths), we walk UP from the caller
|
|
20
|
+
* until we find the `orionfold-relay` package.json. This makes the passed
|
|
21
|
+
* `depth` a hint, not a hard requirement, so all call sites resolve in the
|
|
22
|
+
* bundle without per-site changes.
|
|
23
|
+
*
|
|
24
|
+
* The upward walk verifies `name === "orionfold-relay"` (not just any
|
|
25
|
+
* package.json) so that under npx it anchors to OUR package, never a foreign
|
|
26
|
+
* package.json in the user's launch dir.
|
|
27
|
+
*
|
|
9
28
|
* Uses static `node:` built-in imports. These resolve natively in every
|
|
10
29
|
* server context that consumes this helper — Next.js server modules, the
|
|
11
30
|
* instrumentation hook, and the tsup ESM CLI bundle. (An earlier version used
|
|
@@ -17,8 +36,30 @@ import { join } from "node:path";
|
|
|
17
36
|
*/
|
|
18
37
|
export function getAppRoot(metaDirname: string | undefined, depth: number): string {
|
|
19
38
|
if (metaDirname) {
|
|
39
|
+
// Fast path — source tree: the passed depth lands on the package root.
|
|
20
40
|
const candidate = join(metaDirname, ...Array(depth).fill(".."));
|
|
21
|
-
if (
|
|
41
|
+
if (isPackageRoot(candidate)) return candidate;
|
|
42
|
+
|
|
43
|
+
// Bundle fallback: walk up until we hit the orionfold-relay package root.
|
|
44
|
+
let dir = metaDirname;
|
|
45
|
+
while (true) {
|
|
46
|
+
if (isPackageRoot(dir)) return dir;
|
|
47
|
+
const parent = dirname(dir);
|
|
48
|
+
if (parent === dir) break; // reached filesystem root
|
|
49
|
+
dir = parent;
|
|
50
|
+
}
|
|
22
51
|
}
|
|
23
52
|
return process.cwd();
|
|
24
53
|
}
|
|
54
|
+
|
|
55
|
+
/** True if `dir` holds the orionfold-relay package.json. */
|
|
56
|
+
function isPackageRoot(dir: string): boolean {
|
|
57
|
+
const pkgPath = join(dir, "package.json");
|
|
58
|
+
if (!existsSync(pkgPath)) return false;
|
|
59
|
+
try {
|
|
60
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as { name?: string };
|
|
61
|
+
return pkg.name === PKG_NAME;
|
|
62
|
+
} catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type Database from "better-sqlite3";
|
|
2
|
+
|
|
3
|
+
const OLD_PREFIX = "mcp__ainative__";
|
|
4
|
+
const NEW_PREFIX = "mcp__relay__";
|
|
5
|
+
|
|
6
|
+
export interface McpNamespaceMigrationReport {
|
|
7
|
+
/** Rows in agent_profiles whose allowed_tools were rewritten. */
|
|
8
|
+
profilesUpdated: number;
|
|
9
|
+
/** 1 if the permissions.allow settings row was rewritten, else 0. */
|
|
10
|
+
permissionsUpdated: number;
|
|
11
|
+
errors: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Rewrites the chat/compose MCP tool namespace from the legacy `mcp__ainative__`
|
|
16
|
+
* prefix to `mcp__relay__` in persisted data. Companion to the `engine.ts`
|
|
17
|
+
* server-key flip: that change makes the runtime *publish* tools as
|
|
18
|
+
* `mcp__relay__*`; this migration makes previously-saved allow-lists and
|
|
19
|
+
* "Always Allow" records match the new names, which are compared by exact
|
|
20
|
+
* string (see `permissions.ts:matchesPermission`).
|
|
21
|
+
*
|
|
22
|
+
* Backing stores rewritten:
|
|
23
|
+
* 1. `settings` row `key='permissions.allow'` — a JSON array stored as a
|
|
24
|
+
* string. This is the store that actually carries the namespace today
|
|
25
|
+
* (saved "Always Allow" records). A string-level REPLACE is safe: it
|
|
26
|
+
* rewrites the substring inside the serialized array without parsing it.
|
|
27
|
+
* 2. `agent_profiles.allowed_tools` — a JSON array column. NOTE: in current
|
|
28
|
+
* Relay, profiles are file-based (profile.yaml on disk) and there is no
|
|
29
|
+
* `agent_profiles` table, so this branch is a defensive no-op (guarded by
|
|
30
|
+
* a table-exists check). It is kept so that if a DB-backed profile store
|
|
31
|
+
* is ever reintroduced, the namespace hop is already covered. Shipped
|
|
32
|
+
* profile files carry no `mcp__ainative__` strings (verified), so no file
|
|
33
|
+
* rewrite is needed.
|
|
34
|
+
*
|
|
35
|
+
* Idempotent (a second run matches nothing) and never throws — a missing
|
|
36
|
+
* table (fresh/partial schema) is an expected no-op, not an error. Any real
|
|
37
|
+
* SQL failure is collected in `report.errors` so boot continues visibly.
|
|
38
|
+
*
|
|
39
|
+
* Takes the DB handle explicitly so it can be unit-tested against an in-memory
|
|
40
|
+
* database; production passes the live `sqlite` export.
|
|
41
|
+
*/
|
|
42
|
+
export function migrateMcpNamespace(
|
|
43
|
+
db: Database.Database,
|
|
44
|
+
): McpNamespaceMigrationReport {
|
|
45
|
+
const report: McpNamespaceMigrationReport = {
|
|
46
|
+
profilesUpdated: 0,
|
|
47
|
+
permissionsUpdated: 0,
|
|
48
|
+
errors: [],
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
if (!tableExists(db, "agent_profiles")) {
|
|
52
|
+
// Fresh install — nothing to migrate.
|
|
53
|
+
} else {
|
|
54
|
+
try {
|
|
55
|
+
const r = db
|
|
56
|
+
.prepare(
|
|
57
|
+
`UPDATE agent_profiles
|
|
58
|
+
SET allowed_tools = REPLACE(allowed_tools, ?, ?)
|
|
59
|
+
WHERE allowed_tools LIKE '%' || ? || '%'`,
|
|
60
|
+
)
|
|
61
|
+
.run(OLD_PREFIX, NEW_PREFIX, OLD_PREFIX);
|
|
62
|
+
report.profilesUpdated = r.changes;
|
|
63
|
+
} catch (err) {
|
|
64
|
+
// allowed_tools column may be absent in an older schema — record, don't crash.
|
|
65
|
+
report.errors.push(`agent_profiles rewrite failed: ${String(err)}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!tableExists(db, "settings")) {
|
|
70
|
+
// Fresh install — no saved permissions yet.
|
|
71
|
+
} else {
|
|
72
|
+
try {
|
|
73
|
+
const r = db
|
|
74
|
+
.prepare(
|
|
75
|
+
`UPDATE settings
|
|
76
|
+
SET value = REPLACE(value, ?, ?)
|
|
77
|
+
WHERE key = 'permissions.allow' AND value LIKE '%' || ? || '%'`,
|
|
78
|
+
)
|
|
79
|
+
.run(OLD_PREFIX, NEW_PREFIX, OLD_PREFIX);
|
|
80
|
+
report.permissionsUpdated = r.changes;
|
|
81
|
+
} catch (err) {
|
|
82
|
+
report.errors.push(`settings permissions.allow rewrite failed: ${String(err)}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return report;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function tableExists(db: Database.Database, name: string): boolean {
|
|
90
|
+
const row = db
|
|
91
|
+
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = ?")
|
|
92
|
+
.get(name);
|
|
93
|
+
return row !== undefined;
|
|
94
|
+
}
|
|
@@ -4,6 +4,8 @@ export const createProjectSchema = z.object({
|
|
|
4
4
|
name: z.string().min(1, "Name is required").max(100),
|
|
5
5
|
description: z.string().max(500).optional(),
|
|
6
6
|
workingDirectory: z.string().max(500).optional(),
|
|
7
|
+
// FK to customers.id; null/absent = unlinked. Attributes AI spend to a customer.
|
|
8
|
+
customerId: z.string().min(1).nullish(),
|
|
7
9
|
});
|
|
8
10
|
|
|
9
11
|
export const updateProjectSchema = z.object({
|
|
@@ -11,6 +13,8 @@ export const updateProjectSchema = z.object({
|
|
|
11
13
|
description: z.string().max(500).optional(),
|
|
12
14
|
workingDirectory: z.string().max(500).optional(),
|
|
13
15
|
status: z.enum(["active", "paused", "completed"]).optional(),
|
|
16
|
+
// FK to customers.id; null clears the link, absent leaves it unchanged.
|
|
17
|
+
customerId: z.string().min(1).nullish(),
|
|
14
18
|
});
|
|
15
19
|
|
|
16
20
|
export type CreateProjectInput = z.infer<typeof createProjectSchema>;
|