orionfold-relay 0.15.1 → 0.15.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/dist/cli.js +92 -38
- package/package.json +1 -1
- 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/desktop/sidecar-launch.ts +18 -0
- 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/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.3")) {
|
|
25206
|
+
return "0.15.3";
|
|
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
|
|
@@ -25623,20 +25645,26 @@ function buildNextLaunchArgs({
|
|
|
25623
25645
|
function buildSidecarUrl(port, host = SIDECAR_LOOPBACK_HOST) {
|
|
25624
25646
|
return `http://${host}:${port}`;
|
|
25625
25647
|
}
|
|
25648
|
+
function isNonLoopbackHost(host) {
|
|
25649
|
+
const h = host.trim().toLowerCase();
|
|
25650
|
+
if (h === "localhost" || h === "::1") return false;
|
|
25651
|
+
if (/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h)) return false;
|
|
25652
|
+
return true;
|
|
25653
|
+
}
|
|
25626
25654
|
|
|
25627
25655
|
// bin/cli.ts
|
|
25628
25656
|
init_ainative_paths();
|
|
25629
25657
|
init_bootstrap();
|
|
25630
25658
|
|
|
25631
25659
|
// src/lib/utils/migrate-to-ainative.ts
|
|
25632
|
-
import { existsSync as existsSync3, renameSync, cpSync, rmSync, readFileSync } from "fs";
|
|
25660
|
+
import { existsSync as existsSync3, renameSync, cpSync, rmSync, readFileSync as readFileSync2 } from "fs";
|
|
25633
25661
|
import { join as join5 } from "path";
|
|
25634
25662
|
import { homedir as homedir2 } from "os";
|
|
25635
25663
|
import Database from "better-sqlite3";
|
|
25636
25664
|
function hasSqliteHeader(path19) {
|
|
25637
25665
|
const SQLITE_MAGIC = "SQLite format 3\0";
|
|
25638
25666
|
try {
|
|
25639
|
-
const header =
|
|
25667
|
+
const header = readFileSync2(path19, { encoding: null });
|
|
25640
25668
|
return header.length >= 16 && header.subarray(0, 16).toString("binary") === SQLITE_MAGIC;
|
|
25641
25669
|
} catch {
|
|
25642
25670
|
return false;
|
|
@@ -25758,7 +25786,7 @@ async function migrateLegacyData(options = {}) {
|
|
|
25758
25786
|
|
|
25759
25787
|
// bin/cli.ts
|
|
25760
25788
|
init_detect();
|
|
25761
|
-
var __dirname =
|
|
25789
|
+
var __dirname = dirname5(fileURLToPath3(import.meta.url));
|
|
25762
25790
|
var appDir = join20(__dirname, "..");
|
|
25763
25791
|
var launchCwd2 = process.cwd();
|
|
25764
25792
|
var _envLocalPath = join20(launchCwd2, ".env.local");
|
|
@@ -25766,18 +25794,31 @@ var _firstRunNeedsEnv = !existsSync12(_envLocalPath) && !process.env.RELAY_DATA_
|
|
|
25766
25794
|
if (_firstRunNeedsEnv) {
|
|
25767
25795
|
const folderName = basename5(launchCwd2);
|
|
25768
25796
|
const autoDataDir = join20(homedir8(), `.${folderName}`);
|
|
25769
|
-
|
|
25770
|
-
|
|
25771
|
-
|
|
25797
|
+
try {
|
|
25798
|
+
writeFileSync5(
|
|
25799
|
+
_envLocalPath,
|
|
25800
|
+
`# Auto-created by orionfold-relay on first run.
|
|
25772
25801
|
# Points this folder's install at an isolated data directory.
|
|
25773
25802
|
RELAY_DATA_DIR=${autoDataDir}
|
|
25774
25803
|
`,
|
|
25775
|
-
|
|
25776
|
-
|
|
25777
|
-
|
|
25804
|
+
"utf-8"
|
|
25805
|
+
);
|
|
25806
|
+
console.log(`First run \u2014 wrote ${_envLocalPath} (RELAY_DATA_DIR=${autoDataDir}).`);
|
|
25807
|
+
} catch (e) {
|
|
25808
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
25809
|
+
console.warn(
|
|
25810
|
+
`Warning: could not write ${_envLocalPath} (${reason}).
|
|
25811
|
+
Continuing with the default data directory (~/.relay). This folder will not get an isolated database.`
|
|
25812
|
+
);
|
|
25813
|
+
if (/^([A-Za-z]:)?[\\/]Windows([\\/]|$)/.test(launchCwd2)) {
|
|
25814
|
+
console.warn(
|
|
25815
|
+
`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.`
|
|
25816
|
+
);
|
|
25817
|
+
}
|
|
25818
|
+
}
|
|
25778
25819
|
}
|
|
25779
25820
|
if (existsSync12(_envLocalPath)) {
|
|
25780
|
-
for (const line of
|
|
25821
|
+
for (const line of readFileSync8(_envLocalPath, "utf-8").split("\n")) {
|
|
25781
25822
|
const trimmed = line.trim();
|
|
25782
25823
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
25783
25824
|
const eqIdx = trimmed.indexOf("=");
|
|
@@ -25789,7 +25830,7 @@ if (existsSync12(_envLocalPath)) {
|
|
|
25789
25830
|
}
|
|
25790
25831
|
}
|
|
25791
25832
|
}
|
|
25792
|
-
var pkg = JSON.parse(
|
|
25833
|
+
var pkg = JSON.parse(readFileSync8(join20(appDir, "package.json"), "utf-8"));
|
|
25793
25834
|
function getHelpText() {
|
|
25794
25835
|
const dir = getAinativeDataDir();
|
|
25795
25836
|
const db3 = getAinativeDbPath();
|
|
@@ -25807,6 +25848,7 @@ Environment variables:
|
|
|
25807
25848
|
|
|
25808
25849
|
Examples:
|
|
25809
25850
|
node dist/cli.js --port 3210 --no-open
|
|
25851
|
+
node dist/cli.js --hostname 0.0.0.0 --port 3000 # expose on the LAN (see warning)
|
|
25810
25852
|
node dist/cli.js --data-dir ~/.relay-dogfood --port 3100
|
|
25811
25853
|
node dist/cli.js plugin dry-run my-plugin # print confinement policy
|
|
25812
25854
|
node dist/cli.js pack add ./my-pack # install a Relay pack (folder or git url)
|
|
@@ -25814,7 +25856,11 @@ Examples:
|
|
|
25814
25856
|
node dist/cli.js pack remove my-pack # uninstall a pack
|
|
25815
25857
|
`;
|
|
25816
25858
|
}
|
|
25817
|
-
program.name("relay").description("Orionfold Relay \u2014 a local-first, multi-agent orchestration runtime and builder scaffold for AI-native work.").version(pkg.version).addHelpText("after", getHelpText).option("-p, --port <number>", "port to start on", "3000").option(
|
|
25859
|
+
program.name("relay").description("Orionfold Relay \u2014 a local-first, multi-agent orchestration runtime and builder scaffold for AI-native work.").version(pkg.version).addHelpText("after", getHelpText).option("-p, --port <number>", "port to start on", "3000").option(
|
|
25860
|
+
"--hostname <host>",
|
|
25861
|
+
"host to bind to (default 127.0.0.1; use 0.0.0.0 to expose on the network)",
|
|
25862
|
+
"127.0.0.1"
|
|
25863
|
+
).option("--data-dir <path>", "custom data directory (overrides RELAY_DATA_DIR)").option("--reset", "delete the local database before starting").option("--no-open", "don't auto-open browser").option("--safe-mode", "disable Kind-1 plugin MCP servers; Kind-5 primitives bundles still load");
|
|
25818
25864
|
var firstArg = process.argv[2];
|
|
25819
25865
|
var isPluginSubcommand = firstArg === "plugin";
|
|
25820
25866
|
var isPackSubcommand = firstArg === "pack";
|
|
@@ -25910,8 +25956,8 @@ async function main() {
|
|
|
25910
25956
|
let effectiveCwd = appDir;
|
|
25911
25957
|
const localNm = join20(appDir, "node_modules");
|
|
25912
25958
|
if (!existsSync12(join20(localNm, "next", "package.json"))) {
|
|
25913
|
-
let searchDir =
|
|
25914
|
-
while (searchDir !==
|
|
25959
|
+
let searchDir = dirname5(appDir);
|
|
25960
|
+
while (searchDir !== dirname5(searchDir)) {
|
|
25915
25961
|
const candidate = join20(searchDir, "node_modules", "next", "package.json");
|
|
25916
25962
|
if (existsSync12(candidate)) {
|
|
25917
25963
|
const hoistedRoot = searchDir;
|
|
@@ -25933,22 +25979,29 @@ async function main() {
|
|
|
25933
25979
|
const dest = join20(hoistedRoot, name);
|
|
25934
25980
|
const src = join20(appDir, name);
|
|
25935
25981
|
if (!existsSync12(dest) && existsSync12(src)) {
|
|
25936
|
-
writeFileSync5(dest,
|
|
25982
|
+
writeFileSync5(dest, readFileSync8(src));
|
|
25937
25983
|
}
|
|
25938
25984
|
}
|
|
25939
25985
|
effectiveCwd = hoistedRoot;
|
|
25940
25986
|
break;
|
|
25941
25987
|
}
|
|
25942
|
-
searchDir =
|
|
25988
|
+
searchDir = dirname5(searchDir);
|
|
25943
25989
|
}
|
|
25944
25990
|
}
|
|
25945
25991
|
const nextEntrypoint = resolveNextEntrypoint(effectiveCwd);
|
|
25946
25992
|
const isPrebuilt = existsSync12(join20(effectiveCwd, ".next", "BUILD_ID"));
|
|
25993
|
+
const bindHost = opts.hostname || "127.0.0.1";
|
|
25994
|
+
if (isNonLoopbackHost(bindHost)) {
|
|
25995
|
+
console.warn(
|
|
25996
|
+
`\u26A0 Binding to ${bindHost} \u2014 Relay will be reachable from other machines on the network. It is designed for local-first, single-user use and has no network authentication. Only do this on a trusted network, and put a reverse proxy with auth in front if exposing it more broadly.`
|
|
25997
|
+
);
|
|
25998
|
+
}
|
|
25947
25999
|
const nextArgs = buildNextLaunchArgs({
|
|
25948
26000
|
isPrebuilt,
|
|
25949
|
-
port: actualPort
|
|
26001
|
+
port: actualPort,
|
|
26002
|
+
host: bindHost
|
|
25950
26003
|
});
|
|
25951
|
-
const sidecarUrl = buildSidecarUrl(actualPort);
|
|
26004
|
+
const sidecarUrl = buildSidecarUrl(actualPort, bindHost);
|
|
25952
26005
|
console.log(`Orionfold Relay ${pkg.version} \u2014 Community Edition`);
|
|
25953
26006
|
console.log(`Data dir: ${DATA_DIR}`);
|
|
25954
26007
|
console.log(`Mode: ${isPrebuilt ? "production" : "development"}`);
|
|
@@ -25967,10 +26020,11 @@ async function main() {
|
|
|
25967
26020
|
}
|
|
25968
26021
|
});
|
|
25969
26022
|
if (opts.open !== false) {
|
|
26023
|
+
const openUrl = isNonLoopbackHost(bindHost) ? buildSidecarUrl(actualPort, "127.0.0.1") : sidecarUrl;
|
|
25970
26024
|
setTimeout(async () => {
|
|
25971
26025
|
try {
|
|
25972
26026
|
const open = (await import("open")).default;
|
|
25973
|
-
await open(
|
|
26027
|
+
await open(openUrl);
|
|
25974
26028
|
} catch {
|
|
25975
26029
|
}
|
|
25976
26030
|
}, 3e3);
|
package/package.json
CHANGED
|
@@ -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,
|
|
@@ -83,3 +83,21 @@ export function buildNextLaunchArgs({
|
|
|
83
83
|
export function buildSidecarUrl(port: number, host = SIDECAR_LOOPBACK_HOST): string {
|
|
84
84
|
return `http://${host}:${port}`;
|
|
85
85
|
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* True when `host` is NOT a loopback address — i.e. binding to it exposes the
|
|
89
|
+
* server beyond the local machine. Relay is local-first with light auth, so the
|
|
90
|
+
* CLI warns before binding to such a host (`--hostname 0.0.0.0`, a LAN IP, a
|
|
91
|
+
* hostname, etc.). This is an advisory gate, not access control, so it errs
|
|
92
|
+
* toward warning: anything not provably loopback returns true.
|
|
93
|
+
*
|
|
94
|
+
* Loopback = `localhost`, IPv6 `::1`, or any `127.0.0.0/8` address (Linux lets
|
|
95
|
+
* you bind e.g. `127.0.0.5`). `0.0.0.0` / `::` are INADDR_ANY ("all
|
|
96
|
+
* interfaces") — the most exposing choice — and are treated as non-loopback.
|
|
97
|
+
*/
|
|
98
|
+
export function isNonLoopbackHost(host: string): boolean {
|
|
99
|
+
const h = host.trim().toLowerCase();
|
|
100
|
+
if (h === "localhost" || h === "::1") return false;
|
|
101
|
+
if (/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h)) return false;
|
|
102
|
+
return true;
|
|
103
|
+
}
|
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>;
|