@akanjs/cli 2.3.11-rc.6 → 2.3.11-rc.7

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.
@@ -3577,6 +3577,8 @@ class AppExecutor extends SysExecutor {
3577
3577
  ...routeEnv,
3578
3578
  ...devPort ? { PORT: devPort, AKAN_PUBLIC_CLIENT_PORT: devPort, AKAN_PUBLIC_SERVER_PORT: devPort } : {}
3579
3579
  });
3580
+ if (type === "build")
3581
+ Object.assign(process.env, env);
3580
3582
  return { env };
3581
3583
  }
3582
3584
  #publicEnv = null;
@@ -4996,7 +4998,6 @@ var workflowSyncStatePath = (target) => `${workflowSyncDir}/${syncStateSlug(targ
4996
4998
  var generatedFilePathsForTarget = (targetRoot, reason = "Generated files were refreshed by sync.") => [
4997
4999
  { path: `${targetRoot}/lib/cnst.ts`, action: "sync", reason },
4998
5000
  { path: `${targetRoot}/lib/dict.ts`, action: "sync", reason },
4999
- { path: `${targetRoot}/lib/option.ts`, action: "sync", reason },
5000
5001
  { path: `${targetRoot}/lib/index.ts`, action: "sync", reason }
5001
5002
  ];
5002
5003
  var writeGeneratedSyncState = async (workspace, state) => {
@@ -7650,13 +7651,59 @@ var resourceList = [
7650
7651
  { uri: "akan://workspace/modules", name: "Workspace modules", mimeType: "application/json" }
7651
7652
  ];
7652
7653
  var cursorMcpConfigPath = ".cursor/mcp.json";
7654
+ var claudeMcpConfigPath = ".mcp.json";
7655
+ var codexMcpConfigPath = ".codex/config.toml";
7656
+ var akanMcpInstallTargets = ["cursor", "claude", "codex"];
7657
+ var akanMcpInstallConfigPaths = {
7658
+ cursor: cursorMcpConfigPath,
7659
+ claude: claudeMcpConfigPath,
7660
+ codex: codexMcpConfigPath
7661
+ };
7653
7662
  var cursorWorkspaceFolder = "$" + "{workspaceFolder}";
7663
+ var claudeProjectDir = "$CLAUDE_PROJECT_DIR";
7664
+ var akanMcpCommand = (mode, { cd } = {}) => cd ? `cd "${cd}" && akan mcp --mode ${mode}` : `akan mcp --mode ${mode}`;
7654
7665
  var createAkanCursorMcpServer = (mode = "readonly") => ({
7655
7666
  type: "stdio",
7656
7667
  command: "bash",
7657
- args: ["-lc", `cd "${cursorWorkspaceFolder}" && akan mcp --mode ${mode}`]
7668
+ args: ["-lc", akanMcpCommand(mode, { cd: cursorWorkspaceFolder })]
7658
7669
  });
7670
+ var createAkanClaudeMcpServer = (mode = "readonly") => ({
7671
+ type: "stdio",
7672
+ command: "bash",
7673
+ args: ["-lc", akanMcpCommand(mode, { cd: claudeProjectDir })]
7674
+ });
7675
+ var createAkanMcpServer = (target, mode = "readonly") => target === "cursor" ? createAkanCursorMcpServer(mode) : createAkanClaudeMcpServer(mode);
7659
7676
  var akanCursorMcpServer = createAkanCursorMcpServer();
7677
+ var codexMcpServerTableHeader = "[mcp_servers.akan]";
7678
+ var createAkanCodexMcpServerBlock = (mode = "readonly") => `${codexMcpServerTableHeader}
7679
+ command = "bash"
7680
+ args = ["-lc", "${akanMcpCommand(mode)}"]
7681
+ `;
7682
+ var codexAkanTablePattern = /^\[mcp_servers\.akan\][^\n]*\n(?:(?!\[)[^\n]*(?:\n|$))*/m;
7683
+ var upsertCodexMcpServerBlock = (existing, block, { force = false } = {}) => {
7684
+ const nextBlock = block.endsWith(`
7685
+ `) ? block : `${block}
7686
+ `;
7687
+ const match = existing.match(codexAkanTablePattern);
7688
+ if (!match) {
7689
+ if (!existing.trim())
7690
+ return nextBlock;
7691
+ return `${existing.replace(/\s*$/, "")}
7692
+
7693
+ ${nextBlock}`;
7694
+ }
7695
+ if (match[0].trim() === nextBlock.trim())
7696
+ return existing;
7697
+ if (!force)
7698
+ throw new Error(`${codexMcpConfigPath} already has an "akan" MCP server. Re-run with --force to overwrite it.`);
7699
+ const start = match.index ?? 0;
7700
+ const before = existing.slice(0, start);
7701
+ const after = existing.slice(start + match[0].length);
7702
+ const separator = after && !after.startsWith(`
7703
+ `) ? `
7704
+ ` : "";
7705
+ return `${before}${nextBlock}${separator}${after}`;
7706
+ };
7660
7707
  var renderDoctorText = (result) => {
7661
7708
  const lines = [`Akan doctor status: ${result.status}`];
7662
7709
  if (result.diagnostics.length === 0) {
@@ -7679,7 +7726,6 @@ var generatedFiles = [
7679
7726
  "*/lib/cnst.ts",
7680
7727
  "*/lib/db.ts",
7681
7728
  "*/lib/dict.ts",
7682
- "*/lib/option.ts",
7683
7729
  "*/lib/sig.ts",
7684
7730
  "*/lib/srv.ts",
7685
7731
  "*/lib/st.ts",
package/index.js CHANGED
@@ -3575,6 +3575,8 @@ class AppExecutor extends SysExecutor {
3575
3575
  ...routeEnv,
3576
3576
  ...devPort ? { PORT: devPort, AKAN_PUBLIC_CLIENT_PORT: devPort, AKAN_PUBLIC_SERVER_PORT: devPort } : {}
3577
3577
  });
3578
+ if (type === "build")
3579
+ Object.assign(process.env, env);
3578
3580
  return { env };
3579
3581
  }
3580
3582
  #publicEnv = null;
@@ -4994,7 +4996,6 @@ var workflowSyncStatePath = (target) => `${workflowSyncDir}/${syncStateSlug(targ
4994
4996
  var generatedFilePathsForTarget = (targetRoot, reason = "Generated files were refreshed by sync.") => [
4995
4997
  { path: `${targetRoot}/lib/cnst.ts`, action: "sync", reason },
4996
4998
  { path: `${targetRoot}/lib/dict.ts`, action: "sync", reason },
4997
- { path: `${targetRoot}/lib/option.ts`, action: "sync", reason },
4998
4999
  { path: `${targetRoot}/lib/index.ts`, action: "sync", reason }
4999
5000
  ];
5000
5001
  var writeGeneratedSyncState = async (workspace, state) => {
@@ -7648,13 +7649,59 @@ var resourceList = [
7648
7649
  { uri: "akan://workspace/modules", name: "Workspace modules", mimeType: "application/json" }
7649
7650
  ];
7650
7651
  var cursorMcpConfigPath = ".cursor/mcp.json";
7652
+ var claudeMcpConfigPath = ".mcp.json";
7653
+ var codexMcpConfigPath = ".codex/config.toml";
7654
+ var akanMcpInstallTargets = ["cursor", "claude", "codex"];
7655
+ var akanMcpInstallConfigPaths = {
7656
+ cursor: cursorMcpConfigPath,
7657
+ claude: claudeMcpConfigPath,
7658
+ codex: codexMcpConfigPath
7659
+ };
7651
7660
  var cursorWorkspaceFolder = "$" + "{workspaceFolder}";
7661
+ var claudeProjectDir = "$CLAUDE_PROJECT_DIR";
7662
+ var akanMcpCommand = (mode, { cd } = {}) => cd ? `cd "${cd}" && akan mcp --mode ${mode}` : `akan mcp --mode ${mode}`;
7652
7663
  var createAkanCursorMcpServer = (mode = "readonly") => ({
7653
7664
  type: "stdio",
7654
7665
  command: "bash",
7655
- args: ["-lc", `cd "${cursorWorkspaceFolder}" && akan mcp --mode ${mode}`]
7666
+ args: ["-lc", akanMcpCommand(mode, { cd: cursorWorkspaceFolder })]
7667
+ });
7668
+ var createAkanClaudeMcpServer = (mode = "readonly") => ({
7669
+ type: "stdio",
7670
+ command: "bash",
7671
+ args: ["-lc", akanMcpCommand(mode, { cd: claudeProjectDir })]
7656
7672
  });
7673
+ var createAkanMcpServer = (target, mode = "readonly") => target === "cursor" ? createAkanCursorMcpServer(mode) : createAkanClaudeMcpServer(mode);
7657
7674
  var akanCursorMcpServer = createAkanCursorMcpServer();
7675
+ var codexMcpServerTableHeader = "[mcp_servers.akan]";
7676
+ var createAkanCodexMcpServerBlock = (mode = "readonly") => `${codexMcpServerTableHeader}
7677
+ command = "bash"
7678
+ args = ["-lc", "${akanMcpCommand(mode)}"]
7679
+ `;
7680
+ var codexAkanTablePattern = /^\[mcp_servers\.akan\][^\n]*\n(?:(?!\[)[^\n]*(?:\n|$))*/m;
7681
+ var upsertCodexMcpServerBlock = (existing, block, { force = false } = {}) => {
7682
+ const nextBlock = block.endsWith(`
7683
+ `) ? block : `${block}
7684
+ `;
7685
+ const match = existing.match(codexAkanTablePattern);
7686
+ if (!match) {
7687
+ if (!existing.trim())
7688
+ return nextBlock;
7689
+ return `${existing.replace(/\s*$/, "")}
7690
+
7691
+ ${nextBlock}`;
7692
+ }
7693
+ if (match[0].trim() === nextBlock.trim())
7694
+ return existing;
7695
+ if (!force)
7696
+ throw new Error(`${codexMcpConfigPath} already has an "akan" MCP server. Re-run with --force to overwrite it.`);
7697
+ const start = match.index ?? 0;
7698
+ const before = existing.slice(0, start);
7699
+ const after = existing.slice(start + match[0].length);
7700
+ const separator = after && !after.startsWith(`
7701
+ `) ? `
7702
+ ` : "";
7703
+ return `${before}${nextBlock}${separator}${after}`;
7704
+ };
7658
7705
  var renderDoctorText = (result) => {
7659
7706
  const lines = [`Akan doctor status: ${result.status}`];
7660
7707
  if (result.diagnostics.length === 0) {
@@ -7677,7 +7724,6 @@ var generatedFiles = [
7677
7724
  "*/lib/cnst.ts",
7678
7725
  "*/lib/db.ts",
7679
7726
  "*/lib/dict.ts",
7680
- "*/lib/option.ts",
7681
7727
  "*/lib/sig.ts",
7682
7728
  "*/lib/srv.ts",
7683
7729
  "*/lib/st.ts",
@@ -18092,8 +18138,7 @@ var addEnumFieldWorkflowSpec = {
18092
18138
  predictedChanges: [
18093
18139
  { target: "*/lib/<module>/<module>.constant.ts", action: "modify", reason: "Enum field shape is added." },
18094
18140
  { target: "*/lib/<module>/<module>.dictionary.ts", action: "modify", reason: "Enum labels are added." },
18095
- { target: "*/lib/<module>/<module>.option.ts", action: "modify", reason: "Enum options may be added." },
18096
- { target: "*/lib/option.ts", action: "sync", reason: "Generated option barrel may change after sync." }
18141
+ { target: "*/lib/<module>/<module>.option.ts", action: "modify", reason: "Enum options may be added." }
18097
18142
  ],
18098
18143
  validation: [
18099
18144
  ...baseValidation,
@@ -18498,8 +18543,7 @@ var createScalarWorkflowSpec = {
18498
18543
  }
18499
18544
  ],
18500
18545
  predictedChanges: [
18501
- { target: "*/lib/__scalar/<scalar>/*", action: "create", reason: "New scalar source files are scaffolded." },
18502
- { target: "*/lib/option.ts", action: "sync", reason: "Generated option barrel may include scalar options." }
18546
+ { target: "*/lib/__scalar/<scalar>/*", action: "create", reason: "New scalar source files are scaffolded." }
18503
18547
  ],
18504
18548
  validation: baseValidation,
18505
18549
  completionCriteria: ["Scalar files exist under __scalar.", "Generated files are refreshed.", "Lint passes."]
@@ -20370,14 +20414,18 @@ class ContextRunner extends runner("context") {
20370
20414
  return await Prompter.getInstruction(name);
20371
20415
  }
20372
20416
  async installMcp(workspace, target, { force = false, mode = "readonly" } = {}) {
20373
- if (target !== "cursor")
20374
- throw new Error(`Unknown MCP install target: ${target}. Use cursor.`);
20375
- const existing = await workspace.exists(cursorMcpConfigPath) ? await workspace.readJson(cursorMcpConfigPath) : {};
20417
+ if (target === "codex")
20418
+ return await this.#installCodexMcp(workspace, { force, mode });
20419
+ return await this.#installJsonMcp(workspace, target, { force, mode });
20420
+ }
20421
+ async#installJsonMcp(workspace, target, { force, mode }) {
20422
+ const configPath2 = akanMcpInstallConfigPaths[target];
20423
+ const existing = await workspace.exists(configPath2) ? await workspace.readJson(configPath2) : {};
20376
20424
  const mcpServers = existing.mcpServers ?? {};
20377
20425
  const currentAkanServer = mcpServers.akan;
20378
- const nextAkanServer = createAkanCursorMcpServer(mode);
20426
+ const nextAkanServer = createAkanMcpServer(target, mode);
20379
20427
  if (currentAkanServer && !force && JSON.stringify(currentAkanServer) !== JSON.stringify(nextAkanServer)) {
20380
- throw new Error(`${cursorMcpConfigPath} already has an "akan" MCP server. Re-run with --force to overwrite it.`);
20428
+ throw new Error(`${configPath2} already has an "akan" MCP server. Re-run with --force to overwrite it.`);
20381
20429
  }
20382
20430
  const nextConfig = {
20383
20431
  ...existing,
@@ -20386,9 +20434,15 @@ class ContextRunner extends runner("context") {
20386
20434
  akan: nextAkanServer
20387
20435
  }
20388
20436
  };
20389
- await workspace.writeFile(cursorMcpConfigPath, `${JSON.stringify(nextConfig, null, 2)}
20437
+ await workspace.writeFile(configPath2, `${JSON.stringify(nextConfig, null, 2)}
20390
20438
  `);
20391
- return cursorMcpConfigPath;
20439
+ return configPath2;
20440
+ }
20441
+ async#installCodexMcp(workspace, { force, mode }) {
20442
+ const existing = await workspace.exists(codexMcpConfigPath) ? await workspace.readFile(codexMcpConfigPath) : "";
20443
+ const merged = upsertCodexMcpServerBlock(existing, createAkanCodexMcpServerBlock(mode), { force });
20444
+ await workspace.writeFile(codexMcpConfigPath, merged);
20445
+ return codexMcpConfigPath;
20392
20446
  }
20393
20447
  listMcpTools(mode = "readonly") {
20394
20448
  return listAkanMcpTools(mode);
@@ -20680,13 +20734,26 @@ ${payload}`);
20680
20734
  agent: "`akan agent install <target>` writes editor-specific agent rules with overwrite protection.",
20681
20735
  mcp: "`akan mcp --mode readonly|plan|apply` starts the Akan MCP server over stdio with an explicit permission mode.",
20682
20736
  "mcp-call": '`akan mcp-call <tool> --mode plan --args \'{"workflow":"add-field","inputs":{"app":"demo"}}\'` calls one MCP tool through the same runner path for debugging.',
20683
- "mcp-install": "`akan mcp-install cursor --mode readonly|plan|apply` installs the Akan MCP server config for Cursor."
20737
+ "mcp-install": "`akan mcp-install [cursor|claude|codex|all] --mode readonly|plan|apply` installs the Akan MCP server config for Cursor (.cursor/mcp.json), Claude Code (.mcp.json), and Codex (.codex/config.toml). Defaults to all targets."
20684
20738
  };
20685
20739
  return explanations[command3] ?? `No detailed explanation is available for ${command3}. Run \`akan --help\` for command help.`;
20686
20740
  }
20687
20741
  }
20688
20742
 
20689
20743
  // pkgs/@akanjs/cli/context/context.script.ts
20744
+ var resolveMcpInstallTargets = (target) => {
20745
+ if (!target || target === "all")
20746
+ return [...akanMcpInstallTargets];
20747
+ if (akanMcpInstallTargets.includes(target))
20748
+ return [target];
20749
+ throw new Error(`Unknown MCP install target: ${target}. Use cursor, claude, codex, or all.`);
20750
+ };
20751
+ var mcpTargetLabels = {
20752
+ cursor: "Cursor",
20753
+ claude: "Claude Code",
20754
+ codex: "Codex"
20755
+ };
20756
+
20690
20757
  class ContextScript extends script("context", [ContextRunner]) {
20691
20758
  async context(workspace, options = {}) {
20692
20759
  Logger17.rawLog(await this.contextRunner.getContext(workspace, options));
@@ -20695,11 +20762,15 @@ class ContextScript extends script("context", [ContextRunner]) {
20695
20762
  Logger17.rawLog(await this.contextRunner.doctor(workspace, options));
20696
20763
  }
20697
20764
  async mcpInstall(workspace, target, { force = false, mode = "readonly" } = {}) {
20698
- if (target && target !== "cursor")
20699
- throw new Error(`Unknown MCP install target: ${target}. Use cursor.`);
20700
- const written = await this.contextRunner.installMcp(workspace, "cursor", { force, mode });
20701
- Logger17.rawLog(`Akan MCP server installed for Cursor:
20702
- - ${written}`);
20765
+ const targets = resolveMcpInstallTargets(target);
20766
+ const written = [];
20767
+ for (const t of targets) {
20768
+ const configPath2 = await this.contextRunner.installMcp(workspace, t, { force, mode });
20769
+ written.push(`${mcpTargetLabels[t]}: ${configPath2}`);
20770
+ }
20771
+ Logger17.rawLog(`Akan MCP server installed (${mode} mode):
20772
+ ${written.map((line) => `- ${line}`).join(`
20773
+ `)}`);
20703
20774
  }
20704
20775
  async mcp(workspace, { mode = "readonly" } = {}) {
20705
20776
  await this.contextRunner.runMcp(workspace, { mode });
@@ -20738,7 +20809,7 @@ class ContextCommand extends command("context", [ContextScript], ({ public: targ
20738
20809
  }).option("strict", Boolean, { desc: "treat recommended conventions as errors", default: false }).with(Workspace).exec(async function(format, strict, workspace) {
20739
20810
  await this.contextScript.doctor(workspace, { format, strict });
20740
20811
  }),
20741
- mcpInstall: target({ desc: "Install the Akan MCP server config for Cursor" }).arg("target", String, { desc: "cursor", nullable: true }).option("force", Boolean, { desc: "overwrite an existing Akan MCP server entry", default: false }).option("mode", String, {
20812
+ mcpInstall: target({ desc: "Install the Akan MCP server config for Cursor, Claude Code, and Codex" }).arg("target", String, { desc: "cursor, claude, codex, or all", nullable: true }).option("force", Boolean, { desc: "overwrite an existing Akan MCP server entry", default: false }).option("mode", String, {
20742
20813
  desc: "MCP permission mode",
20743
20814
  default: "readonly",
20744
20815
  enum: ["readonly", "plan", "apply"]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akanjs/cli",
3
- "version": "2.3.11-rc.6",
3
+ "version": "2.3.11-rc.7",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -34,7 +34,7 @@
34
34
  "@langchain/openai": "^1.4.6",
35
35
  "@tailwindcss/node": "^4.3.0",
36
36
  "@trapezedev/project": "^7.1.4",
37
- "akanjs": "2.3.11-rc.6",
37
+ "akanjs": "2.3.11-rc.7",
38
38
  "chalk": "^5.6.2",
39
39
  "commander": "^14.0.3",
40
40
  "daisyui": "5.5.23",
@@ -10,7 +10,7 @@ import * as db from "../db";
10
10
  // Convention: <module>.service.ts — business logic orchestration for a database module.
11
11
  // Extends serve(db.<module>, depsCallback) from akanjs/service — binds to the DB model, receives DI deps.
12
12
  // Auto-generated by akan sync (do not write manually):
13
- // getTask(id), createModel(data), updateModel(id, data), removeModel(id),
13
+ // getTask(id), createTask(data), updateTask(id, data), removeTask(id),
14
14
  // listByStatus(status), searchDocs(text), and all filter+query methods from document.ts.
15
15
  // Manual below: lifecycle hooks, custom business logic methods.
16
16
  // Registered by akan sync into srv.ts barrel.
@@ -11,7 +11,7 @@ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: Dic
11
11
  content: `
12
12
  "use client";
13
13
  import { Field, Layout } from "akanjs/ui";
14
- import { cnst, st, usePage } from "@${scanInfo?.type ?? "apps"}/${dict.sysName}/client";
14
+ import { st, usePage } from "@${scanInfo?.type ?? "apps"}/${dict.sysName}/client";
15
15
 
16
16
  interface GeneralProps {
17
17
  className?: string;
@@ -23,10 +23,9 @@ export const General = ({ className }: GeneralProps) => {
23
23
  return (
24
24
  <Layout.Template className={className}>
25
25
  <Field.Text
26
- label={l("${dict.model}.id")}
27
- desc={l("${dict.model}.id.desc")}
28
- value={${dict.model}Form.id}
29
- onChange={st.do.setIdOn${dict.Model}}
26
+ label={l("${dict.model}.name")}
27
+ value={${dict.model}Form.name}
28
+ onChange={st.do.setNameOn${dict.Model}}
30
29
  />
31
30
  </Layout.Template>
32
31
  );
@@ -17,7 +17,7 @@ export const Card = ({ ${dict.model}, href }: ModelProps<"${dict.model}", cnst.L
17
17
  const { l } = usePage();
18
18
  return (
19
19
  <Link href={href} className="w-full">
20
- <div>{l("${dict.model}.id")}:{${dict.model}.id}</div>
20
+ <div>{l("${dict.model}.name")}: {${dict.model}.name}</div>
21
21
  </Link>
22
22
  );
23
23
  };
@@ -20,7 +20,7 @@ export const General = ({ className, ${dict.model} }: GeneralProps) => {
20
20
  const { l } = usePage();
21
21
  return (
22
22
  <div className={clsx("w-full", className)}>
23
- <div>{l("${dict.model}.id")}: {${dict.model}.id}</div>
23
+ <div>{l("${dict.model}.name")}: {${dict.model}.name}</div>
24
24
  </div>
25
25
  );
26
26
  };
@@ -11,7 +11,9 @@ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: Dic
11
11
  content: `
12
12
  "use client";
13
13
  import { Load } from "akanjs/ui";
14
- import { type cnst, ${dict.Model} } from "@${scanInfo?.type ?? "apps"}/${dict.sysName}/client";
14
+ // Alias the domain namespace so the Card/View exports below never collide with the model name
15
+ // (a model literally named "card" or "view" would otherwise shadow this import).
16
+ import { type cnst, ${dict.Model} as ${dict.Model}Domain } from "@${scanInfo?.type ?? "apps"}/${dict.sysName}/client";
15
17
  import type { ClientInit, ClientView, SliceMeta } from "akanjs/fetch";
16
18
 
17
19
  interface CardProps {
@@ -25,7 +27,7 @@ export const Card = ({ className, init, slice }: CardProps) => {
25
27
  className={className}
26
28
  init={init}
27
29
  renderItem={(${dict.model}) => (
28
- <${dict.Model}.Unit.Card key={${dict.model}.id} href={\`/${dict.model}/\${${dict.model}.id}\`} ${dict.model}={${dict.model}} />
30
+ <${dict.Model}Domain.Unit.Card key={${dict.model}.id} href={\`/${dict.model}/\${${dict.model}.id}\`} ${dict.model}={${dict.model}} />
29
31
  )}
30
32
  />
31
33
  );
@@ -36,7 +38,7 @@ interface ViewProps {
36
38
  view: ClientView<"${dict.model}", cnst.${dict.Model}>;
37
39
  }
38
40
  export const View = ({ view }: ViewProps) => {
39
- return <Load.View view={view} renderView={(${dict.model}) => <${dict.Model}.View.General ${dict.model}={${dict.model}} />} />;
41
+ return <Load.View view={view} renderView={(${dict.model}) => <${dict.Model}Domain.View.General ${dict.model}={${dict.model}} />} />;
40
42
  };
41
43
  `,
42
44
  };
@@ -10,11 +10,12 @@ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: Dic
10
10
  import { via } from "akanjs/constant";
11
11
 
12
12
  export class ${dict.Model}Input extends via((field) => ({
13
+ name: field(String),
13
14
  })) {}
14
15
 
15
16
  export class ${dict.Model}Object extends via(${dict.Model}Input, (field) => ({})) {}
16
17
 
17
- export class Light${dict.Model} extends via(${dict.Model}Object, [] as const, (resolve) => ({})) {}
18
+ export class Light${dict.Model} extends via(${dict.Model}Object, ["name"] as const, (resolve) => ({})) {}
18
19
 
19
20
  export class ${dict.Model} extends via(${dict.Model}Object, Light${dict.Model}, (resolve) => ({})) {}
20
21
 
@@ -24,7 +24,9 @@ export const dictionary = modelDictionary(["en", "ko"])
24
24
  .of((t) =>
25
25
  t(["${modelLabelEn}", "${modelLabelKo}"]).desc(["${modelDescEn}", "${modelDescKo}"])
26
26
  )
27
- .model<${dict.Model}>((t) => ({}))
27
+ .model<${dict.Model}>((t) => ({
28
+ name: t(["Name", "이름"]),
29
+ }))
28
30
  .insight<${dict.Model}Insight>((t) => ({}))
29
31
  .slice<${dict.Model}Slice>((fn) => ({
30
32
  inPublic: fn(["${dict.Model} In Public", "${dict.Model} 공개"]).arg((t) => ({})),
@@ -28,7 +28,6 @@ Common generated files include:
28
28
  - `*/lib/cnst.ts`
29
29
  - `*/lib/db.ts`
30
30
  - `*/lib/dict.ts`
31
- - `*/lib/option.ts`
32
31
  - `*/lib/sig.ts`
33
32
  - `*/lib/srv.ts`
34
33
  - `*/lib/st.ts`
@@ -112,6 +111,12 @@ akan test <%= appName %>
112
111
  akan build <%= appName %>
113
112
  ```
114
113
 
114
+ **Verify endpoints with signal tests, not raw HTTP.** The canonical way to check a query/mutation/slice
115
+ contract is an in-memory signal test (`<model>.signal.test.ts`), using the test fetch harness
116
+ (`getOrSetupSignalTestFetch`) — it is fast, needs no running server, and exercises `fetch.*`, `view/edit/merge<Model>`,
117
+ and slice `init`/`list`/`insight` directly. Prefer it over `curl`: the dev gateway locale-prefixes routes (`/en/...`),
118
+ so hand-rolled HTTP calls against a raw path can redirect unexpectedly. See `akan test <%= appName %>`.
119
+
115
120
  ### Other Frequently Used Commands
116
121
 
117
122
  ```bash
@@ -230,6 +235,7 @@ final fallback when no CLI command covers the change.
230
235
  | Add a pure workflow / integration (e.g., Payment, Email) | `lib/_<service>/` → service, signal, store, dictionary, abstract | `akan sync <name>` |
231
236
  | Add a reusable value type (e.g., Address, WorkHistory) | `lib/__scalar/<type>/` → constant, dictionary, abstract | `akan sync <name>` |
232
237
  | Create a new URL-visitable page | `page/` → `_index.tsx`, `_layout.tsx`, `[param]/_index.tsx` | Rebuild (akan start auto-detects) |
238
+ | Change the app color theme / design tokens | `apps/<app>/page/styles.css` → edit the daisyUI `@plugin "daisyui/theme"` blocks (`light`/`dark`, the `--color-*` variables) | akan start hot-reloads |
233
239
  | Add a form or reusable UI component | `ui/` → PascalCase `.tsx` with `"use client"` if needed | `akan sync <name>` |
234
240
  | Add a React hook or browser helper | `webkit/` → camelCase `.ts` with `"use client"` | `akan sync <name>` |
235
241
  | Add a server-only guard, middleware, or adaptor | `srvkit/` → PascalCase `.ts` | `akan sync <name>` |
@@ -247,6 +253,7 @@ final fallback when no CLI command covers the change.
247
253
  | Skip running `akan sync` after deleting a file | Deleted files remain referenced in barrel exports, causing import errors everywhere. | Run `akan sync <name>` after every file add, remove, or rename |
248
254
  | Use "use client" or `useState`/`useEffect` in pages/*.tsx, *.Unit.tsx, and *.View.tsx files | Server code cannot use React hooks. Wrap in a separate `"use client"` component. | Move hook logic to `webkit/` or a `"use client"` UI component |
249
255
  | Use `<a>` tag for internal navigation between pages | Akan.js uses `<Link>` from `akanjs/ui` for client-side navigation — avoids full page reloads. | `import { Link } from "akanjs/ui"` and use `<Link href="/task">...</Link>` |
256
+ | Name a custom `Endpoint`/`Slice` like a generated CRUD op — `create<Model>`, `update<Model>`, `remove<Model>`, `view<Model>`, `edit<Model>`, `merge<Model>` | These names are already auto-generated. A collision can pass sync/typecheck/build and only fail at runtime. | Pick a distinct verb, e.g. `startTask`/`archiveTask`, never `createTask` for a custom endpoint |
250
257
 
251
258
  ## Generated File Tracker (Quick Reference)
252
259
 
@@ -257,7 +264,6 @@ These files are regenerated by `akan sync` and overwritten on every sync. **Do n
257
264
  | `*/lib/cnst.ts` | All `*/lib/*/**.constant.ts` | Barrel for all constants |
258
265
  | `*/lib/db.ts` | All `*/lib/<model>/*.document.ts` | Barrel for all document models |
259
266
  | `*/lib/dict.ts` | All `*/lib/*/**.dictionary.ts` | Barrel for all dictionaries |
260
- | `*/lib/option.ts` | Generated option helpers | Option helper entry |
261
267
  | `*/lib/sig.ts` | All `*/lib/**/**.signal.ts` | Barrel for all signals |
262
268
  | `*/lib/srv.ts` | All `*/lib/**/**.service.ts` | Barrel for all services |
263
269
  | `*/lib/st.ts` | All `*/lib/**/**.store.ts` | Barrel for all stores |
@@ -338,6 +344,10 @@ Three patterns: injecting an **external adapter** (`use<>()`), another **module'
338
344
  or a **predefined framework adapter** (`plug()`). A field named `<refName>Service` resolves to the service
339
345
  registered under `<refName>` — the `Service`/`Signal` suffix is required and stripped to derive the lookup key.
340
346
 
347
+ > `apps/<app>/lib/option.ts` is a **user-owned** file scaffolded once — edit it to register adapters/DI. Unlike the
348
+ > barrels (`cnst.ts`, `db.ts`, `srv.ts`, …) it is **not** overwritten by `akan sync`, so your `.use(...)` registrations
349
+ > are safe.
350
+
341
351
  **A. Adapter injection via `use<>()` (for external clients / global singletons)**
342
352
 
343
353
  ```typescript
@@ -411,12 +421,19 @@ store's generated `upload<Field>On<Model>(fileList)` action handle the upload.
411
421
 
412
422
  A Slice is a named, filtered data view. Add file entries and connect from a page.
413
423
 
424
+ > **Silent failure — a slice `exec` must return a query descriptor, never an executed list.**
425
+ > Return `this.taskService.queryByStatuses(...)` (the `query<Filter>` builder), **not**
426
+ > `this.taskService.listByStatuses(...)` / `listBy...(...)` (which returns a `Promise<Doc[]>`).
427
+ > Returning an array type-checks but throws at runtime during insight aggregation with the opaque
428
+ > `Error: Unknown document field path: 0`. If you see that error, your slice is returning a list, not a query.
429
+
414
430
  ```typescript
415
431
  // 1. apps/<app>/lib/<model>/<model>.signal.ts — Define the slice
416
432
  export class TaskSlice extends slice(srv.task, (init) => ({
417
433
  inTodo: init()
418
434
  .search("statuses", [cnst.TaskStatus])
419
435
  .exec(function (statuses?) {
436
+ // ✅ query<Filter> — a query descriptor. ❌ listByStatuses(...) returns an array and fails at runtime.
420
437
  return this.taskService.queryByStatuses(statuses ?? ["todo", "inProgress"]);
421
438
  }),
422
439
  })) {}
@@ -595,6 +612,41 @@ For each business question, follow this chain:
595
612
  | What client state is shared? | `store.ts` — `store()` with auto-generated form/insight state + custom actions |
596
613
  | What should users see? | `View.tsx` + `Zone.tsx` (detail/container), `Template.tsx` (forms), `Unit.tsx` (cards), `Util.tsx` (buttons) |
597
614
 
615
+ ## Modeling & Query Gotchas
616
+
617
+ A short list of things the type system does not always catch:
618
+
619
+ - **Slices return a query, not a list.** A slice `exec` must return `this.<model>Service.query<Filter>(...)`, never
620
+ a `list<Filter>(...)` / `listBy...(...)` array. Returning an array type-checks but fails at runtime with
621
+ `Unknown document field path: 0`. (See Recipe 3.)
622
+ - **Custom endpoint names must not collide with generated CRUD.** `create/update/remove/view/edit/merge<Model>`
623
+ already exist. A collision can build green and fail only at runtime — pick a distinct verb.
624
+ - **Numbers are `Int` or `Float`, never `Number`.** `field(Number)` / `.body("x", Number)` fail to typecheck. Use
625
+ `Int` for counts, `Float` for decimals.
626
+ - **Array fields use `field([T])`.** e.g. `tags: field([String])`, `images: field([File])` — not `field(String)` with
627
+ a suffix.
628
+ - **Reading a secret field needs an explicit select.** `field(...).secret()` values (e.g. `passwordHash`) are stripped
629
+ from query results by default. Fetch them with `{ select: { <field>: true } }`, e.g.
630
+ `this.userModel.pickById(id, { select: { passwordHash: true } })`.
631
+
632
+ ## Current User, Guards & Auth-Gated Pages
633
+
634
+ Built-in user authentication (session / JWT / password hashing) ships as a separate Akan auth library, not in the
635
+ core framework. The core framework gives you the composition points below; wire the auth library through them.
636
+
637
+ - **Guards** attach at the signal declaration, not per-method:
638
+ `endpoint(srv.task, { guards: { root: SignedIn } }, ({ mutation }) => ({...}))` or
639
+ `slice(srv.task, { guards: { root: SignedIn, get: Public, cru: Public } }, ...)`. `Public` always allows; other
640
+ guards implement the `Guard` interface in `srvkit/` (server-only) and read the request context.
641
+ - **Read the current user inside a custom endpoint** by injecting an `InternalArg` with `.with(...)`:
642
+ `mutation(cnst.Task).with(CurrentUserId).exec(async function (currentUserId) { ... })`. The `Guard` /
643
+ `InternalArg` helpers live in `srvkit/` and read `context.getHttpContext().req.user`.
644
+ - **Auto-generated CRUD and `serve()` service methods / lifecycle hooks do not receive session context.** If an
645
+ operation needs the acting user, expose a custom endpoint that takes it via `.with(CurrentUserId)` — never trust a
646
+ client-supplied user id.
647
+ - **SSR auth-gated pages: guard at the layout.** Check the session in the `_layout.tsx` loader and redirect when it is
648
+ absent, so nested pages never render for signed-out users.
649
+
598
650
  ## Auto-Generated API Reference
599
651
 
600
652
  akan sync automatically generates APIs across all layers. Only write custom logic — never hand-write what the framework generates.
@@ -618,7 +670,7 @@ akan sync automatically generates APIs across all layers. Only write custom logi
618
670
  |---------------|-------------|
619
671
  | `this.<model>Model` | Auto-injected model adaptor |
620
672
  | `get<Model>(id)`, `load<Model>(id)` | Single document lookup |
621
- | `createModel(data)`, `updateModel(id, data)`, `removeModel(id)` | CRUD operations |
673
+ | `create<Model>(data)`, `update<Model>(id, data)`, `remove<Model>(id)` | CRUD operations — named after the model, e.g. `createTask`/`updateTask`/`removeTask` (there is no literal `createModel`) |
622
674
  | `list<Query>(args)`, `find<Query>(args)`, `pick<Query>(args)` | Filter-based queries |
623
675
  | `exists<Query>(args)`, `count<Query>(args)`, `insight<Query>(args)` | Filter-based helpers |
624
676
  | `_preCreate`, `_postCreate`, `_preUpdate`, `_postUpdate`, `_preRemove`, `_postRemove` | Lifecycle hooks (override to add logic) |
@@ -147,6 +147,10 @@
147
147
  "includes": ["**/*.constant.ts", "**/*.document.ts", "**/*.service.ts", "**/*.store.ts"],
148
148
  "plugins": ["./node_modules/@akanjs/devkit/lint/no-js-private-class-method.grit"]
149
149
  },
150
+ {
151
+ "includes": ["**/*.signal.ts"],
152
+ "plugins": ["./node_modules/@akanjs/devkit/lint/no-redeclare-predefined-endpoint.grit"]
153
+ },
150
154
  {
151
155
  "includes": [
152
156
  "**/*.constant.ts",
@@ -17,7 +17,6 @@ sync or build can overwrite local changes.
17
17
  | `apps/*/lib/cnst.ts` | Re-exports constants and model shapes from module `*.constant.ts` files. |
18
18
  | `apps/*/lib/db.ts` | Re-exports database models from module `*.document.ts` files. |
19
19
  | `apps/*/lib/dict.ts` | Re-exports dictionaries from module `*.dictionary.ts` files. |
20
- | `apps/*/lib/option.ts` | Re-exports generated option helpers. |
21
20
  | `apps/*/lib/srv.ts` | Re-exports services from module `*.service.ts` files. |
22
21
  | `apps/*/lib/sig.ts` | Re-exports endpoints, slices, and internals from module `*.signal.ts` files. |
23
22
  | `apps/*/lib/st.ts` | Re-exports stores from module `*.store.ts` files. |
@@ -13,9 +13,9 @@
13
13
  "agent:setup": "akan agent install all --force",
14
14
  "agent:doctor": "akan doctor --strict --format json",
15
15
  "agent:context": "akan context --format json",
16
- "agent:mcp:readonly": "akan mcp-install cursor --mode readonly --force",
17
- "agent:mcp:plan": "akan mcp-install cursor --mode plan --force",
18
- "agent:mcp:apply": "akan mcp-install cursor --mode apply --force",
16
+ "agent:mcp:readonly": "akan mcp-install all --mode readonly --force",
17
+ "agent:mcp:plan": "akan mcp-install all --mode plan --force",
18
+ "agent:mcp:apply": "akan mcp-install all --mode apply --force",
19
19
  "agent:workflows": "akan workflow list",
20
20
  "agent:sample:context": "akan context --format markdown --app <%= appName %> --module project",
21
21
  "agent:sample:service": "akan create-service billing <%= appName %> --format json",