clay-server 2.46.0 → 2.47.0-beta.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/bin/cli.js +10 -18
- package/lib/daemon.js +49 -8
- package/lib/kiro-defaults.js +22 -0
- package/lib/migrate-single-user.js +286 -0
- package/lib/project-connection.js +1 -0
- package/lib/project-notifications.js +1 -1
- package/lib/project-sessions.js +13 -2
- package/lib/project.js +31 -7
- package/lib/public/app.js +6 -0
- package/lib/public/css/mobile-nav.css +53 -9
- package/lib/public/css/sidebar.css +24 -1
- package/lib/public/css/tui-attention.css +4 -63
- package/lib/public/index.html +4 -0
- package/lib/public/kiro-avatar.svg +14 -0
- package/lib/public/modules/app-messages.js +11 -10
- package/lib/public/modules/app-panels.js +5 -1
- package/lib/public/modules/app-rendering.js +12 -0
- package/lib/public/modules/input.js +5 -3
- package/lib/public/modules/mate-sidebar.js +3 -3
- package/lib/public/modules/session-tui-view.js +4 -40
- package/lib/public/modules/sidebar-mates.js +1 -1
- package/lib/public/modules/sidebar-mobile.js +56 -23
- package/lib/public/modules/sidebar-sessions.js +126 -63
- package/lib/public/modules/tools.js +1 -1
- package/lib/sdk-bridge.js +50 -5
- package/lib/sdk-message-processor.js +4 -2
- package/lib/server-auth.js +14 -5
- package/lib/server.js +4 -0
- package/lib/users-preferences.js +3 -1
- package/lib/users.js +14 -0
- package/lib/yoke/adapters/codex.js +7 -4
- package/lib/yoke/adapters/kiro.js +1156 -0
- package/lib/yoke/index.js +56 -4
- package/lib/yoke/kiro-acp-server.js +328 -0
- package/package.json +2 -1
package/bin/cli.js
CHANGED
|
@@ -1359,24 +1359,16 @@ function setup(callback) {
|
|
|
1359
1359
|
}
|
|
1360
1360
|
port = p;
|
|
1361
1361
|
log(sym.bar);
|
|
1362
|
-
|
|
1362
|
+
// No "single vs multi" question anymore: every deploy runs in the
|
|
1363
|
+
// (general) multi-user model. Solo users get an auto-provisioned
|
|
1364
|
+
// default admin with no PIN and no login wall. OS-level user
|
|
1365
|
+
// isolation stays an opt-in, offered here on Linux and toggleable
|
|
1366
|
+
// later; everything else is just multi-user.
|
|
1367
|
+
askOsUsers("multi");
|
|
1363
1368
|
});
|
|
1364
1369
|
});
|
|
1365
1370
|
}
|
|
1366
1371
|
|
|
1367
|
-
function askMode() {
|
|
1368
|
-
promptSelect("How will you use Clay?", [
|
|
1369
|
-
{ label: "Just me (single user)", value: "single" },
|
|
1370
|
-
{ label: "Multiple users", value: "multi" },
|
|
1371
|
-
], function (mode) {
|
|
1372
|
-
if (mode === "single") {
|
|
1373
|
-
finishSetup(mode, false);
|
|
1374
|
-
} else {
|
|
1375
|
-
askOsUsers(mode);
|
|
1376
|
-
}
|
|
1377
|
-
});
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1380
1372
|
function askOsUsers(mode) {
|
|
1381
1373
|
// Only offer OS user isolation on Linux
|
|
1382
1374
|
if (process.platform !== "linux") {
|
|
@@ -1554,7 +1546,7 @@ async function forkDaemon(mode, keepAwake, extraProjects, addCwd, wantOsUsers) {
|
|
|
1554
1546
|
keepAwake: keepAwake,
|
|
1555
1547
|
dangerouslySkipPermissions: dangerouslySkipPermissions,
|
|
1556
1548
|
osUsers: wantOsUsers || osUsersMode,
|
|
1557
|
-
mode: mode || "
|
|
1549
|
+
mode: mode || "multi",
|
|
1558
1550
|
setupCompleted: true,
|
|
1559
1551
|
projects: allProjects,
|
|
1560
1552
|
});
|
|
@@ -1729,7 +1721,7 @@ async function devMode(mode, keepAwake, existingPinHash, wantOsUsers) {
|
|
|
1729
1721
|
debug: true,
|
|
1730
1722
|
keepAwake: keepAwake || false,
|
|
1731
1723
|
dangerouslySkipPermissions: dangerouslySkipPermissions,
|
|
1732
|
-
mode: mode || "
|
|
1724
|
+
mode: mode || "multi",
|
|
1733
1725
|
setupCompleted: true,
|
|
1734
1726
|
projects: allProjects,
|
|
1735
1727
|
osUsers: wantOsUsers || (prevDevConfig ? (prevDevConfig.osUsers || false) : false),
|
|
@@ -2667,7 +2659,7 @@ var currentVersion = require("../package.json").version;
|
|
|
2667
2659
|
});
|
|
2668
2660
|
} else {
|
|
2669
2661
|
// Reuse existing config (repeat run)
|
|
2670
|
-
await devMode(devConfig.mode || "
|
|
2662
|
+
await devMode(devConfig.mode || "multi", devConfig.keepAwake || false, devConfig.pinHash || null, devConfig.osUsers || false);
|
|
2671
2663
|
}
|
|
2672
2664
|
return;
|
|
2673
2665
|
}
|
|
@@ -2767,7 +2759,7 @@ var currentVersion = require("../package.json").version;
|
|
|
2767
2759
|
|
|
2768
2760
|
if (isRepeatRun || autoYes) {
|
|
2769
2761
|
// Repeat run or --yes: skip wizard, reuse saved config
|
|
2770
|
-
var savedMode = (savedConfig && savedConfig.mode) || "
|
|
2762
|
+
var savedMode = (savedConfig && savedConfig.mode) || "multi";
|
|
2771
2763
|
var savedKeepAwake = (savedConfig && savedConfig.keepAwake) || false;
|
|
2772
2764
|
var savedOsUsers = (savedConfig && savedConfig.osUsers) || false;
|
|
2773
2765
|
|
package/lib/daemon.js
CHANGED
|
@@ -52,6 +52,17 @@ console.log("[daemon] Users: " + usersModule.USERS_FILE);
|
|
|
52
52
|
if (process.env.SUDO_USER) console.log("[daemon] SUDO_USER: " + process.env.SUDO_USER);
|
|
53
53
|
console.log("[daemon] UID: " + (typeof process.getuid === "function" ? process.getuid() : "N/A"));
|
|
54
54
|
|
|
55
|
+
// --- Ensure the multi-user model (there is no single-user runtime anymore):
|
|
56
|
+
// migrate a legacy single-user deploy, or provision a default admin on a fresh
|
|
57
|
+
// one. One-time, idempotent, best-effort. Runs before projects/mates/sessions
|
|
58
|
+
// load so the flip and on-disk backfill are in effect for the rest of boot. ---
|
|
59
|
+
try {
|
|
60
|
+
require("./migrate-single-user").ensureMultiUser();
|
|
61
|
+
config = loadConfig(); // refresh in-memory config to pick up the migration marker
|
|
62
|
+
} catch (e) {
|
|
63
|
+
console.error("[daemon] ensureMultiUser threw (continuing):", e && e.message);
|
|
64
|
+
}
|
|
65
|
+
|
|
55
66
|
// --- OS users mode: check required system dependencies ---
|
|
56
67
|
// Supplementary-group inheritance (issue #367): default on so sessions behave
|
|
57
68
|
// like a normal login; operators can disable via config / the settings toggle.
|
|
@@ -637,6 +648,29 @@ var relay = createServer({
|
|
|
637
648
|
}
|
|
638
649
|
return { ok: false, error: "Project not found" };
|
|
639
650
|
},
|
|
651
|
+
onGetProjectLastVendor: function (slug) {
|
|
652
|
+
for (var i = 0; i < config.projects.length; i++) {
|
|
653
|
+
if (config.projects[i].slug === slug) {
|
|
654
|
+
return { vendor: config.projects[i].lastVendor || null };
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return { vendor: null };
|
|
658
|
+
},
|
|
659
|
+
onSetProjectLastVendor: function (slug, vendor) {
|
|
660
|
+
for (var i = 0; i < config.projects.length; i++) {
|
|
661
|
+
if (config.projects[i].slug === slug) {
|
|
662
|
+
if (config.projects[i].lastVendor === vendor) return { ok: true };
|
|
663
|
+
if (vendor) {
|
|
664
|
+
config.projects[i].lastVendor = vendor;
|
|
665
|
+
} else {
|
|
666
|
+
delete config.projects[i].lastVendor;
|
|
667
|
+
}
|
|
668
|
+
saveConfig(config);
|
|
669
|
+
return { ok: true };
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return { ok: false, error: "Project not found" };
|
|
673
|
+
},
|
|
640
674
|
onGetProjectMcpServers: function (slug) {
|
|
641
675
|
for (var i = 0; i < config.projects.length; i++) {
|
|
642
676
|
if (config.projects[i].slug === slug) {
|
|
@@ -957,21 +991,28 @@ var relay = createServer({
|
|
|
957
991
|
},
|
|
958
992
|
});
|
|
959
993
|
|
|
960
|
-
|
|
961
|
-
|
|
994
|
+
// Reclaim idle out-of-process agent runtimes. Any adapter that owns a child
|
|
995
|
+
// process may implement the optional shutdownIfIdle(ms); adapters without one
|
|
996
|
+
// (Claude, which runs in-process or under sdk-worker) are simply skipped.
|
|
997
|
+
// The CLAY_CODEX_* names are kept as aliases for backwards compatibility.
|
|
998
|
+
var IDLE_MS = Number(process.env.CLAY_ADAPTER_IDLE_MS) || Number(process.env.CLAY_CODEX_IDLE_MS) || 5 * 60 * 1000;
|
|
999
|
+
var REAP_MS = Number(process.env.CLAY_ADAPTER_REAPER_MS) || Number(process.env.CLAY_CODEX_REAPER_MS) || 60 * 1000;
|
|
962
1000
|
var reaperHandle = setInterval(function () {
|
|
963
1001
|
if (!relay || typeof relay.forEachProject !== "function") return;
|
|
964
1002
|
relay.forEachProject(function (ctx) {
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
1003
|
+
if (!ctx || !ctx.adapters) return;
|
|
1004
|
+
Object.keys(ctx.adapters).forEach(function (vendor) {
|
|
1005
|
+
try {
|
|
1006
|
+
var adapter = ctx.adapters[vendor];
|
|
1007
|
+
if (!adapter || typeof adapter.shutdownIfIdle !== "function") return;
|
|
1008
|
+
var result = adapter.shutdownIfIdle(IDLE_MS);
|
|
968
1009
|
if (result && typeof result.catch === "function") {
|
|
969
1010
|
result.catch(function (e) {
|
|
970
|
-
console.error("[daemon]
|
|
1011
|
+
console.error("[daemon] " + vendor + " idle reclaim failed:", e && e.message ? e.message : e);
|
|
971
1012
|
});
|
|
972
1013
|
}
|
|
973
|
-
}
|
|
974
|
-
}
|
|
1014
|
+
} catch (e) {}
|
|
1015
|
+
});
|
|
975
1016
|
});
|
|
976
1017
|
}, REAP_MS);
|
|
977
1018
|
if (reaperHandle && typeof reaperHandle.unref === "function") {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Kiro-specific default values. Single source of truth — do not duplicate
|
|
2
|
+
// elsewhere. Consumed by the server-side vendor plumbing and sent to clients
|
|
3
|
+
// via the config state so the UI renders the right controls.
|
|
4
|
+
|
|
5
|
+
var KIRO_DEFAULTS = {
|
|
6
|
+
// Kiro CLI 2.18.1 exposes the next-generation agent as the v3 engine. Its
|
|
7
|
+
// general-purpose mode id is "vibe" (displayed as "Default").
|
|
8
|
+
engine: "v3",
|
|
9
|
+
mode: "vibe",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function getKiroConfig(sm) {
|
|
13
|
+
return {
|
|
14
|
+
engine: (sm && sm.kiroEngine) || KIRO_DEFAULTS.engine,
|
|
15
|
+
mode: (sm && sm.kiroMode) || KIRO_DEFAULTS.mode,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = {
|
|
20
|
+
KIRO_DEFAULTS: KIRO_DEFAULTS,
|
|
21
|
+
getKiroConfig: getKiroConfig,
|
|
22
|
+
};
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
// One-time migration: fold a legacy single-user deploy into the multi-user
|
|
2
|
+
// model by auto-provisioning one "admin" user that inherits the single-user
|
|
3
|
+
// PIN, profile, and settings. After this, the app is always internally
|
|
4
|
+
// multi-user; a solo deploy is just a one-user multi-user deploy (with the
|
|
5
|
+
// solo auto-login safety net so there's no login wall when no PIN was set).
|
|
6
|
+
//
|
|
7
|
+
// Design goals:
|
|
8
|
+
// - Seamless: existing users upgrade and keep working with no manual steps.
|
|
9
|
+
// Had a PIN -> same PIN still logs in. Had none -> no login wall.
|
|
10
|
+
// - Safe: backs up state first, tolerates partial failure (best-effort per
|
|
11
|
+
// step), and is a no-op on fresh installs and already-multi-user deploys.
|
|
12
|
+
// - Idempotent: a marker in daemon config prevents re-running.
|
|
13
|
+
//
|
|
14
|
+
// NOTE: this touches on-disk state (users.json, daemon.json, profile.json,
|
|
15
|
+
// mates/). It runs once at daemon boot, before the server starts listening.
|
|
16
|
+
|
|
17
|
+
var fs = require("fs");
|
|
18
|
+
var path = require("path");
|
|
19
|
+
var config = require("./config");
|
|
20
|
+
var users = require("./users");
|
|
21
|
+
|
|
22
|
+
var SETTING_KEYS = [
|
|
23
|
+
"chatLayout",
|
|
24
|
+
"autoContinueOnRateLimit",
|
|
25
|
+
"matesEnabled",
|
|
26
|
+
"terminalFont",
|
|
27
|
+
"deletedBuiltinKeys",
|
|
28
|
+
"mateOnboardingShown",
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
function backupFile(filePath, stamp) {
|
|
32
|
+
try {
|
|
33
|
+
if (!fs.existsSync(filePath)) return;
|
|
34
|
+
var bak = filePath + ".pre-migrate-" + stamp + ".bak";
|
|
35
|
+
if (fs.existsSync(bak)) return; // don't clobber an existing backup
|
|
36
|
+
fs.copyFileSync(filePath, bak);
|
|
37
|
+
} catch (e) {
|
|
38
|
+
console.error("[migrate] backup failed for " + filePath + ": " + (e.message || e));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Move legacy flat mates (CONFIG_DIR/mates/*) under the per-user directory
|
|
43
|
+
// (CONFIG_DIR/mates/<userId>/). Best-effort; never fatal. Mates data is
|
|
44
|
+
// preserved on disk regardless.
|
|
45
|
+
function migrateMatesToUser(userId) {
|
|
46
|
+
try {
|
|
47
|
+
var matesRoot = path.join(config.CONFIG_DIR, "mates");
|
|
48
|
+
if (!fs.existsSync(matesRoot)) return;
|
|
49
|
+
var userDir = path.join(matesRoot, userId);
|
|
50
|
+
fs.mkdirSync(userDir, { recursive: true });
|
|
51
|
+
var entries = fs.readdirSync(matesRoot);
|
|
52
|
+
for (var i = 0; i < entries.length; i++) {
|
|
53
|
+
var name = entries[i];
|
|
54
|
+
// Skip per-user dirs (userId-shaped) and our target dir.
|
|
55
|
+
if (name === userId) continue;
|
|
56
|
+
// Only move legacy flat artifacts: mates.json and mate_* directories.
|
|
57
|
+
var isLegacy = name === "mates.json" || name.indexOf("mate_") === 0;
|
|
58
|
+
if (!isLegacy) continue;
|
|
59
|
+
var src = path.join(matesRoot, name);
|
|
60
|
+
var dst = path.join(userDir, name);
|
|
61
|
+
if (fs.existsSync(dst)) continue; // don't clobber freshly-seeded builtins
|
|
62
|
+
try { fs.renameSync(src, dst); }
|
|
63
|
+
catch (e) { console.error("[migrate] mates move skipped for " + name + ": " + (e.message || e)); }
|
|
64
|
+
}
|
|
65
|
+
} catch (e) {
|
|
66
|
+
console.error("[migrate] mates migration failed (non-fatal): " + (e.message || e));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Backfill ownerId onto legacy sessions (created before ownership existed) so
|
|
71
|
+
// they belong to the migrated user. Walks CONFIG_DIR/sessions/**/*.jsonl and
|
|
72
|
+
// patches the first "meta" line in place. Best-effort per file.
|
|
73
|
+
function backfillSessionOwners(userId) {
|
|
74
|
+
var count = 0;
|
|
75
|
+
try {
|
|
76
|
+
var root = path.join(config.CONFIG_DIR, "sessions");
|
|
77
|
+
if (!fs.existsSync(root)) return 0;
|
|
78
|
+
var stack = [root];
|
|
79
|
+
while (stack.length > 0) {
|
|
80
|
+
var dir = stack.pop();
|
|
81
|
+
var entries;
|
|
82
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (e) { continue; }
|
|
83
|
+
for (var i = 0; i < entries.length; i++) {
|
|
84
|
+
var ent = entries[i];
|
|
85
|
+
var full = path.join(dir, ent.name);
|
|
86
|
+
if (ent.isDirectory()) { stack.push(full); continue; }
|
|
87
|
+
if (!ent.name.endsWith(".jsonl")) continue;
|
|
88
|
+
try {
|
|
89
|
+
var content = fs.readFileSync(full, "utf8");
|
|
90
|
+
var nl = content.indexOf("\n");
|
|
91
|
+
var firstLine = nl === -1 ? content : content.slice(0, nl);
|
|
92
|
+
var rest = nl === -1 ? "" : content.slice(nl); // includes leading \n
|
|
93
|
+
var meta;
|
|
94
|
+
try { meta = JSON.parse(firstLine); } catch (e) { continue; }
|
|
95
|
+
if (!meta || meta.type !== "meta") continue;
|
|
96
|
+
if (meta.ownerId) continue; // already owned
|
|
97
|
+
meta.ownerId = userId;
|
|
98
|
+
var tmp = full + ".tmp." + Date.now();
|
|
99
|
+
fs.writeFileSync(tmp, JSON.stringify(meta) + rest);
|
|
100
|
+
fs.renameSync(tmp, full);
|
|
101
|
+
count++;
|
|
102
|
+
} catch (e) { /* skip this file */ }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} catch (e) {
|
|
106
|
+
console.error("[migrate] session ownerId backfill failed (non-fatal): " + (e.message || e));
|
|
107
|
+
}
|
|
108
|
+
return count;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function readProfile() {
|
|
112
|
+
try {
|
|
113
|
+
var p = path.join(config.CONFIG_DIR, "profile.json");
|
|
114
|
+
if (!fs.existsSync(p)) return null;
|
|
115
|
+
var raw = fs.readFileSync(p, "utf8");
|
|
116
|
+
var obj = JSON.parse(raw);
|
|
117
|
+
return (obj && typeof obj === "object") ? obj : null;
|
|
118
|
+
} catch (e) { return null; }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Returns { migrated: true, userId } on success, or { skipped: <reason> }.
|
|
122
|
+
function migrateSingleUserToMulti() {
|
|
123
|
+
try {
|
|
124
|
+
// Guard 1: already multi-user -> nothing to do.
|
|
125
|
+
if (users.isMultiUser()) return { skipped: "already-multi-user" };
|
|
126
|
+
|
|
127
|
+
// Guard 2: users already exist (partial prior run) -> don't double-create.
|
|
128
|
+
var existing = users.getAllUsers();
|
|
129
|
+
if (existing && existing.length > 0) return { skipped: "users-exist" };
|
|
130
|
+
|
|
131
|
+
var cfg = config.loadConfig() || {};
|
|
132
|
+
|
|
133
|
+
// Guard 3: idempotency marker.
|
|
134
|
+
if (cfg.singleUserMigratedAt) return { skipped: "already-migrated" };
|
|
135
|
+
|
|
136
|
+
// Guard 4: only migrate deploys that show prior single-user use. A pristine
|
|
137
|
+
// fresh install (no PIN, no settings, no profile) is left for the later
|
|
138
|
+
// default-flip phase, so this migration is strictly an upgrade path.
|
|
139
|
+
var profile = readProfile();
|
|
140
|
+
var hasSettings = SETTING_KEYS.some(function (k) { return cfg[k] !== undefined; });
|
|
141
|
+
var hasPin = !!cfg.pinHash;
|
|
142
|
+
if (!profile && !hasSettings && !hasPin) return { skipped: "fresh-install" };
|
|
143
|
+
|
|
144
|
+
var stamp = String(Date.now());
|
|
145
|
+
console.log("[migrate] Legacy single-user deploy detected; migrating to multi-user (stamp=" + stamp + ")");
|
|
146
|
+
|
|
147
|
+
// --- Backup before touching anything ---
|
|
148
|
+
backupFile(config.configPath(), stamp);
|
|
149
|
+
// users.json / auth-tokens live in CONFIG_DIR; back them up if present.
|
|
150
|
+
backupFile(path.join(config.CONFIG_DIR, "users.json"), stamp);
|
|
151
|
+
backupFile(path.join(config.CONFIG_DIR, "profile.json"), stamp);
|
|
152
|
+
|
|
153
|
+
// --- Flip to multi-user, then provision the sole admin ---
|
|
154
|
+
// Flip first so createUser's built-in mate seeding writes to the per-user
|
|
155
|
+
// mates path (CONFIG_DIR/mates/<userId>/), not the flat legacy path.
|
|
156
|
+
users.enableMultiUser();
|
|
157
|
+
|
|
158
|
+
var displayName = (profile && profile.name) ? String(profile.name) : "Admin";
|
|
159
|
+
var created = users.createUserWithoutPin({
|
|
160
|
+
username: "admin",
|
|
161
|
+
displayName: displayName,
|
|
162
|
+
role: "admin",
|
|
163
|
+
profile: profile || undefined,
|
|
164
|
+
});
|
|
165
|
+
if (!created || !created.user) {
|
|
166
|
+
console.error("[migrate] Failed to create admin user: " + (created && created.error));
|
|
167
|
+
// Roll the flag back so we don't leave a userless multi-user deploy.
|
|
168
|
+
try { users.disableMultiUser(); } catch (e) {}
|
|
169
|
+
return { skipped: "create-failed" };
|
|
170
|
+
}
|
|
171
|
+
var userId = created.user.id;
|
|
172
|
+
|
|
173
|
+
// --- Carry over the PIN hash directly (preserve the existing credential;
|
|
174
|
+
// we have the hash, not the raw PIN, so we can't go through updateUserPin) ---
|
|
175
|
+
if (hasPin) {
|
|
176
|
+
try {
|
|
177
|
+
var data = users.loadUsers();
|
|
178
|
+
for (var i = 0; i < data.users.length; i++) {
|
|
179
|
+
if (data.users[i].id === userId) { data.users[i].pinHash = cfg.pinHash; break; }
|
|
180
|
+
}
|
|
181
|
+
users.saveUsers(data);
|
|
182
|
+
} catch (e) {
|
|
183
|
+
console.error("[migrate] PIN carry-over failed (user will have no PIN): " + (e.message || e));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// --- Carry over settings ---
|
|
188
|
+
try {
|
|
189
|
+
if (cfg.chatLayout !== undefined && users.setChatLayout) users.setChatLayout(userId, cfg.chatLayout);
|
|
190
|
+
if (cfg.autoContinueOnRateLimit !== undefined && users.setAutoContinue) users.setAutoContinue(userId, cfg.autoContinueOnRateLimit);
|
|
191
|
+
if (cfg.matesEnabled !== undefined && users.setMatesEnabled) users.setMatesEnabled(userId, cfg.matesEnabled);
|
|
192
|
+
if (cfg.terminalFont && users.setTerminalFont) {
|
|
193
|
+
users.setTerminalFont(userId, cfg.terminalFont.family, cfg.terminalFont.size);
|
|
194
|
+
}
|
|
195
|
+
// Fields without a dedicated setter: write directly.
|
|
196
|
+
if (cfg.deletedBuiltinKeys !== undefined || cfg.mateOnboardingShown !== undefined) {
|
|
197
|
+
var d2 = users.loadUsers();
|
|
198
|
+
for (var j = 0; j < d2.users.length; j++) {
|
|
199
|
+
if (d2.users[j].id === userId) {
|
|
200
|
+
if (cfg.deletedBuiltinKeys !== undefined) d2.users[j].deletedBuiltinKeys = cfg.deletedBuiltinKeys;
|
|
201
|
+
if (cfg.mateOnboardingShown !== undefined) d2.users[j].mateOnboardingShown = cfg.mateOnboardingShown;
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
users.saveUsers(d2);
|
|
206
|
+
}
|
|
207
|
+
} catch (e) {
|
|
208
|
+
console.error("[migrate] settings carry-over failed (non-fatal): " + (e.message || e));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// --- Move legacy flat mates under the new user ---
|
|
212
|
+
migrateMatesToUser(userId);
|
|
213
|
+
|
|
214
|
+
// --- Backfill session ownership so legacy sessions belong to the user ---
|
|
215
|
+
var backfilled = backfillSessionOwners(userId);
|
|
216
|
+
if (backfilled > 0) console.log("[migrate] Backfilled ownerId on " + backfilled + " legacy session(s)");
|
|
217
|
+
|
|
218
|
+
// --- Mark done (keep the old daemon-config keys for now; a later phase
|
|
219
|
+
// removes the dual settings path). ---
|
|
220
|
+
try {
|
|
221
|
+
var freshCfg = config.loadConfig() || {};
|
|
222
|
+
freshCfg.singleUserMigratedAt = Date.now();
|
|
223
|
+
freshCfg.singleUserMigratedUserId = userId;
|
|
224
|
+
config.saveConfig(freshCfg);
|
|
225
|
+
} catch (e) {
|
|
226
|
+
console.error("[migrate] Failed to write migration marker: " + (e.message || e));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
console.log("[migrate] Single-user -> multi-user migration complete. admin userId=" + userId + (hasPin ? " (PIN preserved)" : " (no PIN; solo auto-login)"));
|
|
230
|
+
return { migrated: true, userId: userId };
|
|
231
|
+
} catch (e) {
|
|
232
|
+
console.error("[migrate] Single-user migration aborted (non-fatal): " + (e.message || e));
|
|
233
|
+
return { skipped: "error", error: e.message || String(e) };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Guarantee the app is in the multi-user model with at least one user, so the
|
|
238
|
+
// single-user runtime never exists. Called at daemon boot:
|
|
239
|
+
// - already multi-user with users -> nothing to do
|
|
240
|
+
// - legacy single-user with data -> migrate it (preserves PIN/settings)
|
|
241
|
+
// - fresh/empty -> provision one no-PIN "admin"
|
|
242
|
+
// (solo auto-login: no login wall)
|
|
243
|
+
// OS-user isolation stays an orthogonal, settings/flag-driven toggle.
|
|
244
|
+
function ensureMultiUser() {
|
|
245
|
+
try {
|
|
246
|
+
if (users.isMultiUser()) {
|
|
247
|
+
var have = users.getAllUsers();
|
|
248
|
+
if (have && have.length > 0) return { skipped: "ready" };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Try the legacy single-user migration first (handles PIN/settings/mates/
|
|
252
|
+
// session carry-over for existing deploys).
|
|
253
|
+
var mig = migrateSingleUserToMulti();
|
|
254
|
+
if (mig && mig.migrated) return mig;
|
|
255
|
+
|
|
256
|
+
// Fresh or empty deploy: provision a default admin so we're multi-user with
|
|
257
|
+
// one user. No PIN -> the phase-1 solo auto-login skips the login wall.
|
|
258
|
+
var existing = users.getAllUsers();
|
|
259
|
+
if (!existing || existing.length === 0) {
|
|
260
|
+
if (!users.isMultiUser()) users.enableMultiUser();
|
|
261
|
+
var created = users.createUserWithoutPin({
|
|
262
|
+
username: "admin",
|
|
263
|
+
displayName: "Admin",
|
|
264
|
+
role: "admin",
|
|
265
|
+
});
|
|
266
|
+
if (!created || !created.user) {
|
|
267
|
+
console.error("[migrate] Failed to provision default admin: " + (created && created.error));
|
|
268
|
+
return { skipped: "provision-failed" };
|
|
269
|
+
}
|
|
270
|
+
console.log("[migrate] Provisioned default admin (no PIN; solo auto-login). userId=" + created.user.id);
|
|
271
|
+
return { provisioned: true, userId: created.user.id };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Users exist but flag wasn't set (shouldn't normally happen) -> flip it.
|
|
275
|
+
if (!users.isMultiUser()) users.enableMultiUser();
|
|
276
|
+
return { skipped: "flag-fixed" };
|
|
277
|
+
} catch (e) {
|
|
278
|
+
console.error("[migrate] ensureMultiUser failed (non-fatal): " + (e.message || e));
|
|
279
|
+
return { skipped: "error", error: e.message || String(e) };
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
module.exports = {
|
|
284
|
+
migrateSingleUserToMulti: migrateSingleUserToMulti,
|
|
285
|
+
ensureMultiUser: ensureMultiUser,
|
|
286
|
+
};
|
|
@@ -143,6 +143,7 @@ function attachConnection(ctx) {
|
|
|
143
143
|
sendTo(ws, { type: "model_info", model: sm.currentModel, models: initialModels, vendor: initialVendor, availableVendors: sm.availableVendors || [], installedVendors: sm.installedVendors || [] });
|
|
144
144
|
}
|
|
145
145
|
sendTo(ws, { type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
|
|
146
|
+
sendTo(ws, { type: "last_vendor", vendor: sm.lastVendor || "" });
|
|
146
147
|
sendTo(ws, Object.assign({ type: "codex_config" }, getCodexConfig(sm)));
|
|
147
148
|
sendTo(ws, { type: "term_list", terminals: tm.list() });
|
|
148
149
|
// Context sources sent after session is resolved (per-session storage)
|
|
@@ -21,7 +21,7 @@ function generateId() {
|
|
|
21
21
|
var formatters = {
|
|
22
22
|
auth_required: function (data) {
|
|
23
23
|
var vendor = data.vendor || "claude";
|
|
24
|
-
var title = data.title || ((vendor === "codex" ? "Codex" : "Claude Code") + " is not logged in");
|
|
24
|
+
var title = data.title || ((vendor === "codex" ? "Codex" : (vendor === "kiro" ? "Kiro CLI" : "Claude Code")) + " is not logged in");
|
|
25
25
|
return {
|
|
26
26
|
type: "auth_required",
|
|
27
27
|
title: title,
|
package/lib/project-sessions.js
CHANGED
|
@@ -424,13 +424,13 @@ function attachSessions(ctx) {
|
|
|
424
424
|
if (ws._clayUser && usersModule.isMultiUser()) sessionOpts.ownerId = ws._clayUser.id;
|
|
425
425
|
if (msg.sessionVisibility) sessionOpts.sessionVisibility = msg.sessionVisibility;
|
|
426
426
|
if (msg.vendor) sessionOpts.vendor = msg.vendor;
|
|
427
|
-
// Mode resolution: codex sessions are always GUI (no TUI adapter).
|
|
427
|
+
// Mode resolution: codex and kiro sessions are always GUI (no TUI adapter).
|
|
428
428
|
// Claude sessions honor the explicit msg.mode if provided, otherwise
|
|
429
429
|
// fall back to the user's claudeOpenMode preference. This is what
|
|
430
430
|
// makes the sidebar's "Claude" icon button create the right kind of
|
|
431
431
|
// session without the client needing to know the preference.
|
|
432
432
|
var requestedMode;
|
|
433
|
-
if (msg.vendor === "codex") {
|
|
433
|
+
if (msg.vendor === "codex" || msg.vendor === "kiro") {
|
|
434
434
|
requestedMode = "gui";
|
|
435
435
|
} else if (msg.mode === "tui" || msg.mode === "gui") {
|
|
436
436
|
requestedMode = msg.mode;
|
|
@@ -495,6 +495,17 @@ function attachSessions(ctx) {
|
|
|
495
495
|
newSess = sm.createSession(sessionOpts, ws);
|
|
496
496
|
}
|
|
497
497
|
ws._clayActiveSession = newSess.localId;
|
|
498
|
+
// Remember the vendor only when the client asked for one explicitly.
|
|
499
|
+
// new_session without a vendor (mate sidebar, notification banner,
|
|
500
|
+
// debate) falls through to the default adapter and must not clobber
|
|
501
|
+
// the user's remembered pick.
|
|
502
|
+
if (msg.vendor && sm.lastVendor !== msg.vendor) {
|
|
503
|
+
sm.lastVendor = msg.vendor;
|
|
504
|
+
if (typeof opts.onSetProjectLastVendor === "function") {
|
|
505
|
+
opts.onSetProjectLastVendor(slug, msg.vendor);
|
|
506
|
+
}
|
|
507
|
+
send({ type: "last_vendor", vendor: msg.vendor });
|
|
508
|
+
}
|
|
498
509
|
// Apply project-level email defaults to new session
|
|
499
510
|
if (typeof ctx._email === "object" && ctx._email.getEmailDefaults) {
|
|
500
511
|
var emailDefaults = ctx._email.getEmailDefaults();
|
package/lib/project.js
CHANGED
|
@@ -166,7 +166,7 @@ function createProjectContext(opts) {
|
|
|
166
166
|
var sessionTitleMigrationScheduled = false;
|
|
167
167
|
|
|
168
168
|
// --- YOKE adapters (multi-vendor, lazy init) ---
|
|
169
|
-
var _yokeState = yoke.createAdapters({ cwd: cwd, slug: slug });
|
|
169
|
+
var _yokeState = yoke.createAdapters({ cwd: cwd, slug: slug, osUsers: osUsers });
|
|
170
170
|
var adapters = _yokeState.adapters;
|
|
171
171
|
var defaultVendor = adapters.claude ? "claude" : Object.keys(adapters)[0] || "claude";
|
|
172
172
|
var adapter = adapters[defaultVendor] || null;
|
|
@@ -428,6 +428,12 @@ function createProjectContext(opts) {
|
|
|
428
428
|
var _srvEffort = typeof opts.onGetServerDefaultEffort === "function" ? opts.onGetServerDefaultEffort() : null;
|
|
429
429
|
sm.currentEffort = (_projEffort && _projEffort.effort) || (_srvEffort && _srvEffort.effort) || "medium";
|
|
430
430
|
|
|
431
|
+
// Last vendor the user started a session with in this project. Seeds the
|
|
432
|
+
// sidebar's "New session" button so it defaults to whatever they used last
|
|
433
|
+
// instead of always launching Claude.
|
|
434
|
+
var _projLastVendor = typeof opts.onGetProjectLastVendor === "function" ? opts.onGetProjectLastVendor(slug) : null;
|
|
435
|
+
sm.lastVendor = (_projLastVendor && _projLastVendor.vendor) || null;
|
|
436
|
+
|
|
431
437
|
var _projModel = typeof opts.onGetProjectDefaultModel === "function" ? opts.onGetProjectDefaultModel(slug) : null;
|
|
432
438
|
var _srvModel = typeof opts.onGetServerDefaultModel === "function" ? opts.onGetServerDefaultModel() : null;
|
|
433
439
|
sm._savedDefaultModel = (_projModel && _projModel.model) || (_srvModel && _srvModel.model) || null;
|
|
@@ -897,10 +903,12 @@ function createProjectContext(opts) {
|
|
|
897
903
|
(async function() {
|
|
898
904
|
if (msg.vendor) {
|
|
899
905
|
try {
|
|
906
|
+
var modelLinuxUser = getLinuxUserForWs(ws);
|
|
900
907
|
var vendorAdapter = adapters[msg.vendor] || null;
|
|
901
908
|
if (!vendorAdapter) {
|
|
902
909
|
vendorAdapter = await yoke.lazyCreateAdapter(adapters, msg.vendor, {
|
|
903
910
|
cwd: cwd,
|
|
911
|
+
linuxUser: modelLinuxUser || undefined,
|
|
904
912
|
clayPort: serverPort,
|
|
905
913
|
clayTls: serverTls,
|
|
906
914
|
clayAuthToken: serverAuthToken,
|
|
@@ -1528,14 +1536,30 @@ function createProjectContext(opts) {
|
|
|
1528
1536
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1529
1537
|
} catch (e) {}
|
|
1530
1538
|
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1539
|
+
// Shut down every adapter that owns a child process. shutdown() is
|
|
1540
|
+
// optional on the YOKE contract, so adapters without one are skipped.
|
|
1541
|
+
var shutdowns = [];
|
|
1542
|
+
if (adapters) {
|
|
1543
|
+
Object.keys(adapters).forEach(function(vendor) {
|
|
1544
|
+
var adapter = adapters[vendor];
|
|
1545
|
+
if (!adapter || typeof adapter.shutdown !== "function") return;
|
|
1546
|
+
// Shared adapter instances (e.g. Claude) are reused across projects,
|
|
1547
|
+
// so tearing one down here would kill other projects' sessions.
|
|
1548
|
+
if (adapter.shared) return;
|
|
1549
|
+
try {
|
|
1550
|
+
shutdowns.push(Promise.resolve(adapter.shutdown()).catch(function(err) {
|
|
1551
|
+
console.error("[project] " + vendor + " shutdown failed for " + slug + ":", err && err.message ? err.message : err);
|
|
1552
|
+
return false;
|
|
1553
|
+
}));
|
|
1554
|
+
} catch (err) {
|
|
1555
|
+
console.error("[project] " + vendor + " shutdown threw for " + slug + ":", err && err.message ? err.message : err);
|
|
1556
|
+
}
|
|
1536
1557
|
});
|
|
1537
1558
|
}
|
|
1538
|
-
return
|
|
1559
|
+
if (!shutdowns.length) return Promise.resolve(true);
|
|
1560
|
+
return Promise.all(shutdowns).then(function(results) {
|
|
1561
|
+
return results.every(function(r) { return r !== false; });
|
|
1562
|
+
});
|
|
1539
1563
|
}
|
|
1540
1564
|
|
|
1541
1565
|
// --- Status info ---
|
package/lib/public/app.js
CHANGED
|
@@ -310,6 +310,12 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
310
310
|
// panels
|
|
311
311
|
currentModel: "",
|
|
312
312
|
currentModels: [],
|
|
313
|
+
// Project's last-used vendor; seeds the sidebar's "New session" button.
|
|
314
|
+
lastVendor: "",
|
|
315
|
+
// How Claude sessions open: "gui" (default) or "tui". The server sends
|
|
316
|
+
// claude_open_mode_changed on connect; this seeds it beforehand so the
|
|
317
|
+
// new-session menu doesn't flash the TUI-only entry.
|
|
318
|
+
claudeOpenMode: "gui",
|
|
313
319
|
currentMode: "default",
|
|
314
320
|
currentEffort: "medium",
|
|
315
321
|
currentBetas: [],
|