@kybernesis/create 0.13.4 → 0.14.1

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/init.js CHANGED
@@ -5,6 +5,8 @@ import { CHANNEL_KINDS, channelPlan, engineerPlan, envExample, evalFileTs, exeEv
5
5
  import { suiteDir } from "./skills.js";
6
6
  import { configureArcana } from "./arcana.js";
7
7
  import { upsertEnv } from "./envfile.js";
8
+ import { systemdRestartCommand } from "./systemd.js";
9
+ import { sandboxCleanupScaffoldFiles } from "./sandbox-cleanup.js";
8
10
  /**
9
11
  * The always-installed core. Everything else — channels, subagents, engineer,
10
12
  * host bindings — is opt-in, because assuming them means the FDE deletes files
@@ -252,24 +254,23 @@ export async function init(rawName, options = {}) {
252
254
  console.log(yellow(" ! could not install scripts/claude-subscription.sh — copy it from node_modules/@kybernesis/exe/scripts/"));
253
255
  }
254
256
  /**
255
- * Point the management routes at that script.
257
+ * Point management restarts at the one supervisor that owns production.
256
258
  *
257
- * The registry template ships `restartCommand` commented out next to an
258
- * example path, because on Vercel there is nothing to restart. On a VM
259
- * there is, and the correct value is not a guess — it is the script three
260
- * lines above. Left commented, every install through Studio succeeds and
261
- * then reports that a restart is still required, which reads as a broken
262
- * feature rather than one line of config nobody was asked for.
259
+ * install-service.sh installs systemd with a build-before-start gate. Using
260
+ * eve-server.sh after that would create a second supervisor and can leave
261
+ * two executors racing over one durable store. `-n` makes missing sudo
262
+ * authorization fail instead of hanging a detached Studio restart.
263
263
  */
264
264
  const manageChannelFile = join(dir, "agent/channels/kyb.ts");
265
265
  if (existsSync(manageChannelFile)) {
266
266
  const appRoot = `/home/exedev/${name}`;
267
+ const restartCommand = systemdRestartCommand(name);
267
268
  const wired = readFileSync(manageChannelFile, "utf8").replace(/export default manageChannel\(\{[\s\S]*?\}\);/, () => `export default manageChannel({\n` +
268
269
  ` appRoot: process.env.EVE_APP_DIR ?? ${JSON.stringify(appRoot)},\n` +
269
- ` restartCommand: "bash scripts/eve-server.sh restart",\n` +
270
+ ` restartCommand: ${JSON.stringify(restartCommand)},\n` +
270
271
  `});`);
271
272
  writeFileSync(manageChannelFile, wired);
272
- console.log(dim(" agent/channels/kyb.ts — restart wired to that script"));
273
+ console.log(dim(` agent/channels/kyb.ts — restart wired to systemd (${restartCommand})`));
273
274
  }
274
275
  }
275
276
  if (plan.file) {
@@ -297,6 +298,11 @@ export async function init(rawName, options = {}) {
297
298
  }
298
299
  }
299
300
  }
301
+ for (const file of sandboxCleanupScaffoldFiles(host, depts)) {
302
+ const full = join(dir, file.path);
303
+ mkdirSync(join(full, ".."), { recursive: true });
304
+ writeFileSync(full, file.content);
305
+ }
300
306
  if (engPlan) {
301
307
  for (const file of engPlan.files) {
302
308
  const full = join(dir, file.path);
@@ -0,0 +1,29 @@
1
+ /**
2
+ * eve 0.39 removed `glob` and `grep` from the default tool set (changelog
3
+ * 4c1bd80). Two things follow for an agent written against 0.38:
4
+ *
5
+ * - A `tools/glob.ts` or `tools/grep.ts` that exports `disableTool()` now
6
+ * disables a slot nothing provides. On 0.38 that compiled with a warning;
7
+ * from 0.39 it is a compile error — the whole agent fails to build, and
8
+ * `eve eval` exits 1 with that one line as the only output. Kyber's ten
9
+ * knowledge specialists each carried both files.
10
+ * - A scope that HAD the two tools by default (the root, the builder) loses
11
+ * them silently. The certified agent's behaviour is preserved by opting
12
+ * those scopes back in explicitly, which eve supports with a one-line file.
13
+ *
14
+ * Both happen in one pass over every agent scope, and only in the direction
15
+ * that preserves what the agent already did.
16
+ */
17
+ export declare const REMOVED_DEFAULT_TOOLS: readonly ["glob", "grep"];
18
+ /**
19
+ * A scope that disables `bash` has opted out of the sandbox altogether (the
20
+ * five sandbox tools proxy into one container; disabling bash is how a
21
+ * knowledge specialist stays container-free). Such a scope never had a
22
+ * reason to keep glob or grep either.
23
+ */
24
+ export declare function scopeHasSandboxTools(scope: string): boolean;
25
+ export interface RemovedDefaultToolsRepair {
26
+ removed: string[];
27
+ optedIn: string[];
28
+ }
29
+ export declare function repairRemovedDefaultTools(cwd: string): RemovedDefaultToolsRepair;
@@ -0,0 +1,86 @@
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { join, relative } from "node:path";
3
+ /**
4
+ * eve 0.39 removed `glob` and `grep` from the default tool set (changelog
5
+ * 4c1bd80). Two things follow for an agent written against 0.38:
6
+ *
7
+ * - A `tools/glob.ts` or `tools/grep.ts` that exports `disableTool()` now
8
+ * disables a slot nothing provides. On 0.38 that compiled with a warning;
9
+ * from 0.39 it is a compile error — the whole agent fails to build, and
10
+ * `eve eval` exits 1 with that one line as the only output. Kyber's ten
11
+ * knowledge specialists each carried both files.
12
+ * - A scope that HAD the two tools by default (the root, the builder) loses
13
+ * them silently. The certified agent's behaviour is preserved by opting
14
+ * those scopes back in explicitly, which eve supports with a one-line file.
15
+ *
16
+ * Both happen in one pass over every agent scope, and only in the direction
17
+ * that preserves what the agent already did.
18
+ */
19
+ export const REMOVED_DEFAULT_TOOLS = ["glob", "grep"];
20
+ function optInSource(tool) {
21
+ return (`// eve 0.39 removed ${tool} from the default tool set. This scope had it on the\n` +
22
+ `// certified 0.38 stack, so it is opted back in explicitly (kyb upgrade wrote this).\n` +
23
+ `export { default } from "eve/tools/${tool}";\n`);
24
+ }
25
+ function isDisableToolFile(path) {
26
+ try {
27
+ return /disableTool\s*\(/.test(readFileSync(path, "utf8"));
28
+ }
29
+ catch {
30
+ return false;
31
+ }
32
+ }
33
+ function agentScopes(cwd) {
34
+ const root = join(cwd, "agent");
35
+ if (!existsSync(root))
36
+ return [];
37
+ const scopes = [];
38
+ const visit = (dir) => {
39
+ if (existsSync(join(dir, "agent.ts")))
40
+ scopes.push(dir);
41
+ const subagents = join(dir, "subagents");
42
+ if (!existsSync(subagents))
43
+ return;
44
+ for (const entry of readdirSync(subagents, { withFileTypes: true })) {
45
+ if (entry.isDirectory())
46
+ visit(join(subagents, entry.name));
47
+ }
48
+ };
49
+ visit(root);
50
+ return scopes;
51
+ }
52
+ /**
53
+ * A scope that disables `bash` has opted out of the sandbox altogether (the
54
+ * five sandbox tools proxy into one container; disabling bash is how a
55
+ * knowledge specialist stays container-free). Such a scope never had a
56
+ * reason to keep glob or grep either.
57
+ */
58
+ export function scopeHasSandboxTools(scope) {
59
+ const bash = join(scope, "tools", "bash.ts");
60
+ return !(existsSync(bash) && isDisableToolFile(bash));
61
+ }
62
+ export function repairRemovedDefaultTools(cwd) {
63
+ const removed = [];
64
+ const optedIn = [];
65
+ for (const scope of agentScopes(cwd)) {
66
+ const tools = join(scope, "tools");
67
+ for (const tool of REMOVED_DEFAULT_TOOLS) {
68
+ const file = join(tools, `${tool}.ts`);
69
+ if (existsSync(file)) {
70
+ if (isDisableToolFile(file)) {
71
+ unlinkSync(file);
72
+ removed.push(relative(cwd, file));
73
+ }
74
+ // Anything else authored there (an opt-in, a custom tool) is the
75
+ // agent's own and stays.
76
+ continue;
77
+ }
78
+ if (!scopeHasSandboxTools(scope))
79
+ continue;
80
+ mkdirSync(tools, { recursive: true });
81
+ writeFileSync(file, optInSource(tool));
82
+ optedIn.push(relative(cwd, file));
83
+ }
84
+ }
85
+ return { removed, optedIn };
86
+ }
@@ -0,0 +1,18 @@
1
+ export declare const SANDBOX_CLEANUP_HOOK_FILE = "sandbox-cleanup.ts";
2
+ export declare function terminalSandboxCleanupHookTs(): string;
3
+ export declare function sandboxCleanupScaffoldFiles(host: "vercel" | "exe", subagents: string[]): Array<{
4
+ path: string;
5
+ content: string;
6
+ }>;
7
+ /**
8
+ * Add the dedicated managed hook where it is missing, without replacing an
9
+ * authored file that happens to use the same path.
10
+ *
11
+ * Only scopes that can have a sandbox get it. `ctx.getSandbox()` PROVISIONS
12
+ * a sandbox when the session has none (eve docs, "Delete a sandbox": the next
13
+ * call provisions a fresh workspace), so a cleanup hook on a scope that has
14
+ * disabled its sandbox tools would start a container at session end in order
15
+ * to delete it — on Kyber, ten knowledge specialists doing that per closed
16
+ * session.
17
+ */
18
+ export declare function repairTerminalSandboxCleanupHooks(cwd: string): string[];
@@ -0,0 +1,57 @@
1
+ import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { scopeHasSandboxTools } from "./removed-default-tools.js";
4
+ export const SANDBOX_CLEANUP_HOOK_FILE = "sandbox-cleanup.ts";
5
+ export function terminalSandboxCleanupHookTs() {
6
+ return `export { terminalSandboxCleanupHook as default } from "@kybernesis/exe/sandbox-cleanup";\n`;
7
+ }
8
+ export function sandboxCleanupScaffoldFiles(host, subagents) {
9
+ if (host !== "exe")
10
+ return [];
11
+ return ["agent", ...subagents.map((name) => join("agent/subagents", name))].map((scope) => ({
12
+ path: join(scope, "hooks", SANDBOX_CLEANUP_HOOK_FILE),
13
+ content: terminalSandboxCleanupHookTs(),
14
+ }));
15
+ }
16
+ function findLocalSubagents(directory) {
17
+ if (!existsSync(directory))
18
+ return [];
19
+ const found = [];
20
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
21
+ if (!entry.isDirectory())
22
+ continue;
23
+ const child = join(directory, entry.name);
24
+ if (existsSync(join(child, "agent.ts")))
25
+ found.push(child);
26
+ found.push(...findLocalSubagents(child));
27
+ }
28
+ return found;
29
+ }
30
+ /**
31
+ * Add the dedicated managed hook where it is missing, without replacing an
32
+ * authored file that happens to use the same path.
33
+ *
34
+ * Only scopes that can have a sandbox get it. `ctx.getSandbox()` PROVISIONS
35
+ * a sandbox when the session has none (eve docs, "Delete a sandbox": the next
36
+ * call provisions a fresh workspace), so a cleanup hook on a scope that has
37
+ * disabled its sandbox tools would start a container at session end in order
38
+ * to delete it — on Kyber, ten knowledge specialists doing that per closed
39
+ * session.
40
+ */
41
+ export function repairTerminalSandboxCleanupHooks(cwd) {
42
+ const agent = join(cwd, "agent");
43
+ if (!existsSync(agent))
44
+ return [];
45
+ const scopes = [agent, ...findLocalSubagents(join(agent, "subagents"))].filter(scopeHasSandboxTools);
46
+ const added = [];
47
+ for (const scope of scopes) {
48
+ const hooks = join(scope, "hooks");
49
+ const target = join(hooks, SANDBOX_CLEANUP_HOOK_FILE);
50
+ if (existsSync(target))
51
+ continue;
52
+ mkdirSync(hooks, { recursive: true });
53
+ writeFileSync(target, terminalSandboxCleanupHookTs());
54
+ added.push(target);
55
+ }
56
+ return added;
57
+ }
@@ -0,0 +1,41 @@
1
+ export declare const LEGACY_RESTART_COMMAND = "bash scripts/eve-server.sh restart";
2
+ export declare function systemdRestartCommand(name: string): string;
3
+ export interface ManageRestartDiagnosis {
4
+ verdict: "pass" | "warn" | "fail";
5
+ label: string;
6
+ detail?: string;
7
+ }
8
+ export declare function diagnoseManageRestart(serviceName: string | null, restartCommand: string | undefined): ManageRestartDiagnosis;
9
+ export interface UnitValues {
10
+ name: string;
11
+ user: string;
12
+ app: string;
13
+ port: string;
14
+ }
15
+ export declare function parseAgentServiceUnit(path: string, contents: string): UnitValues | null;
16
+ export type RestartMigration = "absent" | "missing" | "migrated" | "systemd" | "custom" | "blocked";
17
+ export declare function reconcileManageRestart(cwd: string, name: string): RestartMigration;
18
+ export interface InstalledAgentService {
19
+ path: string;
20
+ contents: string;
21
+ values: UnitValues;
22
+ mode: number;
23
+ }
24
+ export declare function findMatchingAgentServiceUnit(cwd: string, systemdDir?: string): InstalledAgentService | null;
25
+ /**
26
+ * Refresh an existing package-generated unit. Deliberately never creates a
27
+ * missing unit, enables it, or restarts it; upgrade only reconciles artifacts
28
+ * whose systemd ownership is already established.
29
+ */
30
+ /**
31
+ * Once systemd owns the agent, a Studio install that restarts through
32
+ * scripts/eve-server.sh starts a second supervisor beside it: the script
33
+ * kills systemd's child and starts its own while Restart=always starts
34
+ * another, and two executors race over one durable store. Migrate the exact
35
+ * scaffolded literal in agent/channels/kyb.ts to the systemd restart; leave
36
+ * anything an operator wrote, with the exact line to use.
37
+ *
38
+ * The unit itself is package-managed by repairHostArtifacts (managed marker,
39
+ * .bak copy); this only follows what that unit implies for restarts.
40
+ */
41
+ export declare function repairManageRestart(cwd: string, deps: Record<string, string>, systemdDir?: string): RestartMigration | null;
@@ -0,0 +1,136 @@
1
+ import { readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
2
+ import { basename, join, resolve } from "node:path";
3
+ import { dim, green, yellow } from "./util.js";
4
+ export const LEGACY_RESTART_COMMAND = "bash scripts/eve-server.sh restart";
5
+ export function systemdRestartCommand(name) {
6
+ return `sudo -n systemctl restart ${name}-agent`;
7
+ }
8
+ export function diagnoseManageRestart(serviceName, restartCommand) {
9
+ if (!serviceName) {
10
+ return restartCommand
11
+ ? { verdict: "pass", label: "management routes can restart the agent after an install" }
12
+ : {
13
+ verdict: "warn",
14
+ label: "management routes have no restartCommand",
15
+ detail: "installing edits this repo and rebuilds; without a restart the response says a restart is still required. Install the systemd service, then use the exact restartCommand its installer prints",
16
+ };
17
+ }
18
+ const desired = systemdRestartCommand(serviceName);
19
+ const remediation = `set restartCommand in agent/channels/kyb.ts to ${JSON.stringify(desired)}. ` +
20
+ `Verify passwordless sudo with: ${desired}. systemctl restart does not wait for an in-flight turn; an install can interrupt it.`;
21
+ if (restartCommand === LEGACY_RESTART_COMMAND) {
22
+ return {
23
+ verdict: "fail",
24
+ label: "management restart uses eve-server.sh while systemd owns the agent",
25
+ detail: `this can start two servers against one durable store; ${remediation}`,
26
+ };
27
+ }
28
+ if (restartCommand === desired) {
29
+ return { verdict: "pass", label: `management routes restart the systemd owner (${serviceName}-agent)` };
30
+ }
31
+ if (restartCommand) {
32
+ return {
33
+ verdict: "warn",
34
+ label: `management restartCommand is customized and was preserved (${JSON.stringify(restartCommand)})`,
35
+ detail: `confirm it cannot start a second supervisor; ${remediation}`,
36
+ };
37
+ }
38
+ return { verdict: "warn", label: "management routes have no restartCommand", detail: remediation };
39
+ }
40
+ export function parseAgentServiceUnit(path, contents) {
41
+ const fileName = /^(.+)-agent\.service$/.exec(basename(path))?.[1];
42
+ const user = /^User=(.+)$/m.exec(contents)?.[1];
43
+ const app = /^WorkingDirectory=(.+)$/m.exec(contents)?.[1];
44
+ const port = /^Environment=PORT=(.+)$/m.exec(contents)?.[1];
45
+ if (!fileName || !user || !app || !port)
46
+ return null;
47
+ return { name: fileName, user, app, port };
48
+ }
49
+ export function reconcileManageRestart(cwd, name) {
50
+ const path = join(cwd, "agent/channels/kyb.ts");
51
+ let before;
52
+ try {
53
+ before = readFileSync(path, "utf8");
54
+ }
55
+ catch {
56
+ return "absent";
57
+ }
58
+ const property = /\brestartCommand\s*:\s*(["'`])([^"'`]+)\1/m.exec(before);
59
+ const configured = property?.[2];
60
+ if (!configured)
61
+ return "missing";
62
+ if (configured === systemdRestartCommand(name))
63
+ return "systemd";
64
+ if (configured !== LEGACY_RESTART_COMMAND || property?.[1] !== '"')
65
+ return "custom";
66
+ try {
67
+ const replacement = property[0].replace(`${property[1]}${configured}${property[1]}`, JSON.stringify(systemdRestartCommand(name)));
68
+ writeFileSync(path, before.slice(0, property.index) + replacement + before.slice(property.index + property[0].length));
69
+ return "migrated";
70
+ }
71
+ catch {
72
+ return "blocked";
73
+ }
74
+ }
75
+ export function findMatchingAgentServiceUnit(cwd, systemdDir = "/etc/systemd/system") {
76
+ try {
77
+ for (const file of readdirSync(systemdDir).filter((entry) => entry.endsWith("-agent.service"))) {
78
+ const path = join(systemdDir, file);
79
+ const contents = readFileSync(path, "utf8");
80
+ const values = parseAgentServiceUnit(path, contents);
81
+ if (values && resolve(values.app) === resolve(cwd)) {
82
+ return { path, contents, values, mode: statSync(path).mode & 0o777 };
83
+ }
84
+ }
85
+ }
86
+ catch {
87
+ return null;
88
+ }
89
+ return null;
90
+ }
91
+ function printRestartGuidance(state, cwd, name) {
92
+ if (state === "absent" || state === "systemd")
93
+ return;
94
+ const desired = systemdRestartCommand(name);
95
+ if (state === "migrated") {
96
+ console.log(` ${green("+")} migrated agent/channels/kyb.ts to ${desired}`);
97
+ }
98
+ else if (state === "custom" || state === "blocked") {
99
+ console.log(` ${yellow("!")} ${state === "blocked" ? "could not migrate" : "preserved customized"} restartCommand in agent/channels/kyb.ts. systemd owns ` +
100
+ `this agent; scripts/eve-server.sh would create a second supervisor. Review it and use exactly:\n` +
101
+ ` ${dim(`restartCommand: ${JSON.stringify(desired)},`)}\n` +
102
+ ` ${dim(`Verify passwordless sudo with: ${desired}`)}\n` +
103
+ ` ${dim("systemctl restart does not wait for an in-flight turn; an install can interrupt it.")}`);
104
+ }
105
+ else {
106
+ console.log(` ${yellow("!")} ${join(cwd, "agent/channels/kyb.ts")} has no restartCommand. Add exactly:\n` +
107
+ ` ${dim(`restartCommand: ${JSON.stringify(desired)},`)}\n` +
108
+ ` ${dim("It needs passwordless sudo and can interrupt an in-flight turn.")}`);
109
+ }
110
+ }
111
+ /**
112
+ * Refresh an existing package-generated unit. Deliberately never creates a
113
+ * missing unit, enables it, or restarts it; upgrade only reconciles artifacts
114
+ * whose systemd ownership is already established.
115
+ */
116
+ /**
117
+ * Once systemd owns the agent, a Studio install that restarts through
118
+ * scripts/eve-server.sh starts a second supervisor beside it: the script
119
+ * kills systemd's child and starts its own while Restart=always starts
120
+ * another, and two executors race over one durable store. Migrate the exact
121
+ * scaffolded literal in agent/channels/kyb.ts to the systemd restart; leave
122
+ * anything an operator wrote, with the exact line to use.
123
+ *
124
+ * The unit itself is package-managed by repairHostArtifacts (managed marker,
125
+ * .bak copy); this only follows what that unit implies for restarts.
126
+ */
127
+ export function repairManageRestart(cwd, deps, systemdDir = "/etc/systemd/system") {
128
+ if (!deps["@kybernesis/exe"])
129
+ return null;
130
+ const match = findMatchingAgentServiceUnit(cwd, systemdDir);
131
+ if (!match)
132
+ return null;
133
+ const state = reconcileManageRestart(cwd, match.values.name);
134
+ printRestartGuidance(state, cwd, match.values.name);
135
+ return state;
136
+ }
@@ -50,3 +50,15 @@ export declare function engineerPlan(host: HostKind, model: string): EngineerPla
50
50
  * endpoint, so none of the subscription backend's constraints apply.
51
51
  */
52
52
  export declare function exeEvalConfigTs(): string;
53
+ /**
54
+ * An eve 0.49 memory slot backed by Arcana.
55
+ *
56
+ * Written by `kyb add memory`. The slot gives the agent recall before every
57
+ * turn without a skill telling the model to go and look; capture stays off
58
+ * because the remember skill already stores facts deliberately. When the
59
+ * Arcana extension is mounted the slot offers no tools of its own — the
60
+ * extension's `arcana_*` set is the same brain through the same key.
61
+ */
62
+ export declare function arcanaMemorySlotTs(options: {
63
+ extensionMounted: boolean;
64
+ }): string;
package/dist/templates.js CHANGED
@@ -383,8 +383,9 @@ export function hostSteps(host, name) {
383
383
  `Attach an LLM integration (ChatGPT subscription, your API key, or exe's gateway):`,
384
384
  ` ssh exe.dev integrations setup chatgpt --name work # once, device-code`,
385
385
  ` ssh exe.dev integrations edit llm --openai=chatgpt --openai-account=work`,
386
- `Install Node 24 + deps on the VM, then: npx eve build && bash scripts/eve-server.sh start`,
387
- `\`eve start\` does NOT read .env.local — scripts/eve-server.sh loads it for you`,
386
+ `Install Node 24 + deps on the VM, fill .env.local, then install the production service:`,
387
+ ` bash node_modules/@kybernesis/exe/scripts/install-service.sh`,
388
+ `The systemd unit exports .env.local, builds before every start, and is the single supervisor`,
388
389
  ];
389
390
  }
390
391
  return [
@@ -571,6 +572,12 @@ export { default } from "@kybernesis/engineer";
571
572
  content: workshopSandbox(host),
572
573
  },
573
574
  ];
575
+ if (onExe) {
576
+ files.push({
577
+ path: "agent/subagents/builder/hooks/sandbox-cleanup.ts",
578
+ content: `export { terminalSandboxCleanupHook as default } from "@kybernesis/exe/sandbox-cleanup";\n`,
579
+ });
580
+ }
574
581
  if (onExe) {
575
582
  files.push({
576
583
  path: "agent/subagents/builder/tools/preview.ts",
@@ -627,3 +634,47 @@ export default defineEvalConfig({
627
634
  });
628
635
  `;
629
636
  }
637
+ /**
638
+ * An eve 0.49 memory slot backed by Arcana.
639
+ *
640
+ * Written by `kyb add memory`. The slot gives the agent recall before every
641
+ * turn without a skill telling the model to go and look; capture stays off
642
+ * because the remember skill already stores facts deliberately. When the
643
+ * Arcana extension is mounted the slot offers no tools of its own — the
644
+ * extension's `arcana_*` set is the same brain through the same key.
645
+ */
646
+ export function arcanaMemorySlotTs(options) {
647
+ return `import { defineMemory } from "eve/memory";
648
+ import { byPrincipal } from "eve/memory/scope";
649
+ import { arcanaMemory } from "@kybernesis/arcana/memory";
650
+
651
+ /**
652
+ * Recall before every turn, from the company brain. eve resolves who the
653
+ * memory belongs to (byPrincipal: the authenticated caller; local-dev while
654
+ * developing) and delivers what Arcana finds as one message the model sees
655
+ * before it answers. Greetings and other short turns skip recall.
656
+ *
657
+ * Capture is off: this agent remembers deliberately through arcana_remember.
658
+ * Set capture: { enabled: true } for an agent that should learn passively.
659
+ */
660
+ const COMPANY = process.env.ARCANA_COMPANY_WORKSPACE!;
661
+ const DM = process.env.ARCANA_DM_WORKSPACE ?? COMPANY;
662
+
663
+ export default defineMemory({
664
+ description: "Durable company memory in Arcana: people, decisions, projects, preferences.",
665
+ provider: arcanaMemory({
666
+ // Keys are workspace-scoped. The eval script points every workspace at the
667
+ // -eval brain, which has its own key; the same choice the extension makes.
668
+ apiKey:
669
+ (COMPANY.endsWith("-eval") ? process.env.ARCANA_EVAL_API_KEY : undefined) ??
670
+ process.env.ARCANA_API_KEY!,
671
+ workspace: COMPANY,
672
+ // A DM is a private surface with its own brain; verified from session auth,
673
+ // never from the message.
674
+ resolveWorkspace: (ctx) =>
675
+ ctx.session.auth.current?.attributes.surface === "dm" ? DM : undefined,${options.extensionMounted ? "\n // The Arcana extension already mounts the full arcana_* tool set.\n tools: false," : ""}
676
+ }),
677
+ scope: byPrincipal,
678
+ });
679
+ `;
680
+ }