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

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.
@@ -1710,7 +1710,8 @@ var appRootAllowedFiles = new Set([
1710
1710
  "main.ts",
1711
1711
  "package.json",
1712
1712
  "server.ts",
1713
- "tsconfig.json"
1713
+ "tsconfig.json",
1714
+ "tsconfig.tsbuildinfo"
1714
1715
  ]);
1715
1716
  var appRootAllowedDirs = new Set([
1716
1717
  ".akan",
@@ -15556,7 +15557,8 @@ class AkanQualityScanner {
15556
15557
  #scanGlobalQuality(sourceFiles) {
15557
15558
  const exportedFunctionLikes = sourceFiles.flatMap((sourceFile2) => getExportedFunctionLikes(sourceFile2));
15558
15559
  const warnings = [];
15559
- for (const [name, declarations] of groupBy(exportedFunctionLikes, (declaration) => declaration.name)) {
15560
+ const nameCheckedDeclarations = exportedFunctionLikes.filter((declaration) => !declaration.duplicateNameExempt);
15561
+ for (const [name, declarations] of groupBy(nameCheckedDeclarations, (declaration) => declaration.name)) {
15560
15562
  if (declarations.length < 2)
15561
15563
  continue;
15562
15564
  warnings.push({
@@ -15710,6 +15712,7 @@ function formatQualityLocation(file, line) {
15710
15712
  }
15711
15713
  function getExportedFunctionLikes(sourceFile2) {
15712
15714
  const declarations = [];
15715
+ const pageExempt = isPageRouteFile(sourceFile2.file);
15713
15716
  for (const statement of sourceFile2.sourceFile.statements) {
15714
15717
  if (ts11.isFunctionDeclaration(statement) && statement.name && isExported(statement)) {
15715
15718
  declarations.push({
@@ -15717,7 +15720,8 @@ function getExportedFunctionLikes(sourceFile2) {
15717
15720
  kind: "function",
15718
15721
  file: sourceFile2.file,
15719
15722
  line: getLine(sourceFile2.sourceFile, statement),
15720
- bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement.body)
15723
+ bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement.body),
15724
+ duplicateNameExempt: pageExempt || isConventionDuplicateNameExempt(sourceFile2.file, false)
15721
15725
  });
15722
15726
  }
15723
15727
  if (ts11.isClassDeclaration(statement) && statement.name && isExported(statement)) {
@@ -15726,7 +15730,8 @@ function getExportedFunctionLikes(sourceFile2) {
15726
15730
  kind: "class",
15727
15731
  file: sourceFile2.file,
15728
15732
  line: getLine(sourceFile2.sourceFile, statement),
15729
- bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement)
15733
+ bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement),
15734
+ duplicateNameExempt: pageExempt || isConventionDuplicateNameExempt(sourceFile2.file, isEnumClassStatement(sourceFile2.sourceFile, statement))
15730
15735
  });
15731
15736
  }
15732
15737
  if (ts11.isVariableStatement(statement) && isExported(statement)) {
@@ -15738,13 +15743,40 @@ function getExportedFunctionLikes(sourceFile2) {
15738
15743
  kind: "function-variable",
15739
15744
  file: sourceFile2.file,
15740
15745
  line: getLine(sourceFile2.sourceFile, declaration),
15741
- bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, declaration.initializer)
15746
+ bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, declaration.initializer),
15747
+ duplicateNameExempt: pageExempt || isConventionDuplicateNameExempt(sourceFile2.file, false)
15742
15748
  });
15743
15749
  }
15744
15750
  }
15745
15751
  }
15746
15752
  return declarations;
15747
15753
  }
15754
+ function isPageRouteFile(file) {
15755
+ const segments = file.split("/");
15756
+ return (segments[0] === "apps" || segments[0] === "libs") && segments[2] === "page";
15757
+ }
15758
+ function isConventionDuplicateNameExempt(file, isEnumClass) {
15759
+ if (!isInLibModule(file))
15760
+ return false;
15761
+ if (file.endsWith(".tsx"))
15762
+ return true;
15763
+ if (file.endsWith(".document.ts") || file.endsWith(".service.ts") || file.endsWith(".signal.ts") || file.endsWith(".store.ts"))
15764
+ return true;
15765
+ if (file.endsWith(".constant.ts"))
15766
+ return !isEnumClass;
15767
+ return false;
15768
+ }
15769
+ function isInLibModule(file) {
15770
+ const segments = file.split("/");
15771
+ return (segments[0] === "apps" || segments[0] === "libs") && segments.includes("lib");
15772
+ }
15773
+ function isEnumClassStatement(sourceFile2, statement) {
15774
+ if (!ts11.isClassDeclaration(statement))
15775
+ return false;
15776
+ const heritageClause = statement.heritageClauses?.find((clause) => clause.token === ts11.SyntaxKind.ExtendsKeyword);
15777
+ const expression = heritageClause?.types[0]?.expression;
15778
+ return !!expression && expression.getText(sourceFile2).startsWith("enumOf(");
15779
+ }
15748
15780
  function getExportedClassNames(sourceFile2) {
15749
15781
  return sourceFile2.statements.filter((statement) => ts11.isClassDeclaration(statement) && !!statement.name).filter((statement) => isExported(statement)).map((statement) => statement.name.text);
15750
15782
  }
@@ -15763,11 +15795,7 @@ function getPlaceholderExportWarnings(sourceFile2) {
15763
15795
  function getDictionaryTextWarnings(sourceFile2) {
15764
15796
  if (!sourceFile2.file.endsWith(".dictionary.ts"))
15765
15797
  return [];
15766
- const stalePatterns = [
15767
- { pattern: /\b[A-Z][A-Za-z0-9]* description\b/, label: "scaffold description text" },
15768
- { pattern: /settting/, label: "misspelling: settting" },
15769
- { pattern: /\uBC30\uB108 \uC218/, label: "stale copied Korean domain noun: \uBC30\uB108 \uC218" }
15770
- ];
15798
+ const stalePatterns = [{ pattern: /\b[A-Z][A-Za-z0-9]* description\b/, label: "scaffold description text" }];
15771
15799
  return stalePatterns.flatMap(({ pattern, label }) => findPatternLines(sourceFile2.content, pattern).map((line) => ({
15772
15800
  rule: "akan.file.dictionary-stale-text",
15773
15801
  scope: "file",
package/index.js CHANGED
@@ -1708,7 +1708,8 @@ var appRootAllowedFiles = new Set([
1708
1708
  "main.ts",
1709
1709
  "package.json",
1710
1710
  "server.ts",
1711
- "tsconfig.json"
1711
+ "tsconfig.json",
1712
+ "tsconfig.tsbuildinfo"
1712
1713
  ]);
1713
1714
  var appRootAllowedDirs = new Set([
1714
1715
  ".akan",
@@ -15554,7 +15555,8 @@ class AkanQualityScanner {
15554
15555
  #scanGlobalQuality(sourceFiles) {
15555
15556
  const exportedFunctionLikes = sourceFiles.flatMap((sourceFile2) => getExportedFunctionLikes(sourceFile2));
15556
15557
  const warnings = [];
15557
- for (const [name, declarations] of groupBy(exportedFunctionLikes, (declaration) => declaration.name)) {
15558
+ const nameCheckedDeclarations = exportedFunctionLikes.filter((declaration) => !declaration.duplicateNameExempt);
15559
+ for (const [name, declarations] of groupBy(nameCheckedDeclarations, (declaration) => declaration.name)) {
15558
15560
  if (declarations.length < 2)
15559
15561
  continue;
15560
15562
  warnings.push({
@@ -15708,6 +15710,7 @@ function formatQualityLocation(file, line) {
15708
15710
  }
15709
15711
  function getExportedFunctionLikes(sourceFile2) {
15710
15712
  const declarations = [];
15713
+ const pageExempt = isPageRouteFile(sourceFile2.file);
15711
15714
  for (const statement of sourceFile2.sourceFile.statements) {
15712
15715
  if (ts11.isFunctionDeclaration(statement) && statement.name && isExported(statement)) {
15713
15716
  declarations.push({
@@ -15715,7 +15718,8 @@ function getExportedFunctionLikes(sourceFile2) {
15715
15718
  kind: "function",
15716
15719
  file: sourceFile2.file,
15717
15720
  line: getLine(sourceFile2.sourceFile, statement),
15718
- bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement.body)
15721
+ bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement.body),
15722
+ duplicateNameExempt: pageExempt || isConventionDuplicateNameExempt(sourceFile2.file, false)
15719
15723
  });
15720
15724
  }
15721
15725
  if (ts11.isClassDeclaration(statement) && statement.name && isExported(statement)) {
@@ -15724,7 +15728,8 @@ function getExportedFunctionLikes(sourceFile2) {
15724
15728
  kind: "class",
15725
15729
  file: sourceFile2.file,
15726
15730
  line: getLine(sourceFile2.sourceFile, statement),
15727
- bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement)
15731
+ bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement),
15732
+ duplicateNameExempt: pageExempt || isConventionDuplicateNameExempt(sourceFile2.file, isEnumClassStatement(sourceFile2.sourceFile, statement))
15728
15733
  });
15729
15734
  }
15730
15735
  if (ts11.isVariableStatement(statement) && isExported(statement)) {
@@ -15736,13 +15741,40 @@ function getExportedFunctionLikes(sourceFile2) {
15736
15741
  kind: "function-variable",
15737
15742
  file: sourceFile2.file,
15738
15743
  line: getLine(sourceFile2.sourceFile, declaration),
15739
- bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, declaration.initializer)
15744
+ bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, declaration.initializer),
15745
+ duplicateNameExempt: pageExempt || isConventionDuplicateNameExempt(sourceFile2.file, false)
15740
15746
  });
15741
15747
  }
15742
15748
  }
15743
15749
  }
15744
15750
  return declarations;
15745
15751
  }
15752
+ function isPageRouteFile(file) {
15753
+ const segments = file.split("/");
15754
+ return (segments[0] === "apps" || segments[0] === "libs") && segments[2] === "page";
15755
+ }
15756
+ function isConventionDuplicateNameExempt(file, isEnumClass) {
15757
+ if (!isInLibModule(file))
15758
+ return false;
15759
+ if (file.endsWith(".tsx"))
15760
+ return true;
15761
+ if (file.endsWith(".document.ts") || file.endsWith(".service.ts") || file.endsWith(".signal.ts") || file.endsWith(".store.ts"))
15762
+ return true;
15763
+ if (file.endsWith(".constant.ts"))
15764
+ return !isEnumClass;
15765
+ return false;
15766
+ }
15767
+ function isInLibModule(file) {
15768
+ const segments = file.split("/");
15769
+ return (segments[0] === "apps" || segments[0] === "libs") && segments.includes("lib");
15770
+ }
15771
+ function isEnumClassStatement(sourceFile2, statement) {
15772
+ if (!ts11.isClassDeclaration(statement))
15773
+ return false;
15774
+ const heritageClause = statement.heritageClauses?.find((clause) => clause.token === ts11.SyntaxKind.ExtendsKeyword);
15775
+ const expression = heritageClause?.types[0]?.expression;
15776
+ return !!expression && expression.getText(sourceFile2).startsWith("enumOf(");
15777
+ }
15746
15778
  function getExportedClassNames(sourceFile2) {
15747
15779
  return sourceFile2.statements.filter((statement) => ts11.isClassDeclaration(statement) && !!statement.name).filter((statement) => isExported(statement)).map((statement) => statement.name.text);
15748
15780
  }
@@ -15761,11 +15793,7 @@ function getPlaceholderExportWarnings(sourceFile2) {
15761
15793
  function getDictionaryTextWarnings(sourceFile2) {
15762
15794
  if (!sourceFile2.file.endsWith(".dictionary.ts"))
15763
15795
  return [];
15764
- const stalePatterns = [
15765
- { pattern: /\b[A-Z][A-Za-z0-9]* description\b/, label: "scaffold description text" },
15766
- { pattern: /settting/, label: "misspelling: settting" },
15767
- { pattern: /\uBC30\uB108 \uC218/, label: "stale copied Korean domain noun: \uBC30\uB108 \uC218" }
15768
- ];
15796
+ const stalePatterns = [{ pattern: /\b[A-Z][A-Za-z0-9]* description\b/, label: "scaffold description text" }];
15769
15797
  return stalePatterns.flatMap(({ pattern, label }) => findPatternLines(sourceFile2.content, pattern).map((line) => ({
15770
15798
  rule: "akan.file.dictionary-stale-text",
15771
15799
  scope: "file",
@@ -16028,26 +16056,61 @@ var targetPaths = {
16028
16056
  "agents-md": "AGENTS.md",
16029
16057
  claude: "CLAUDE.md"
16030
16058
  };
16031
- var targetTitle = {
16032
- cursor: "Akan Cursor Rules",
16033
- "agents-md": "Akan Agent Rules",
16034
- claude: "Akan Claude Rules"
16059
+ var AGENT_BLOCK_START = "<!-- akan:agent:start -->";
16060
+ var AGENT_BLOCK_END = "<!-- akan:agent:end -->";
16061
+ var SAMPLE_ARTIFACTS = [
16062
+ { probe: "lib/task/task.constant.ts", target: "lib/task", label: "sample database module" },
16063
+ { probe: "lib/_noti/noti.service.ts", target: "lib/_noti", label: "sample service module" },
16064
+ {
16065
+ probe: "lib/__scalar/workHistory/workHistory.dictionary.ts",
16066
+ target: "lib/__scalar/workHistory",
16067
+ label: "sample scalar module"
16068
+ },
16069
+ { probe: "page/task/_index.tsx", target: "page/task", label: "sample task pages" }
16070
+ ];
16071
+ var DEFAULT_INDEX_MARKER = "Akan.js template";
16072
+ var renderSampleCleanup = async (workspace, appNames) => {
16073
+ const items = [];
16074
+ for (const appName of appNames) {
16075
+ for (const artifact2 of SAMPLE_ARTIFACTS) {
16076
+ if (await workspace.exists(`apps/${appName}/${artifact2.probe}`)) {
16077
+ items.push(`- \`apps/${appName}/${artifact2.target}\` \u2014 ${artifact2.label}; delete it once you no longer need the reference.`);
16078
+ }
16079
+ }
16080
+ const indexPath = `apps/${appName}/page/_index.tsx`;
16081
+ if (await workspace.exists(indexPath)) {
16082
+ const content = await workspace.readFile(indexPath).catch(() => "");
16083
+ if (content.includes(DEFAULT_INDEX_MARKER)) {
16084
+ items.push(`- \`${indexPath}\` \u2014 default Akan landing page; replace it with your own home page.`);
16085
+ }
16086
+ }
16087
+ }
16088
+ if (items.length === 0)
16089
+ return "";
16090
+ return `## Start Clean (Remove Scaffolded Samples)
16091
+
16092
+ This workspace was scaffolded with reference samples so the Akan conventions are visible in real code. They are
16093
+ not part of your product. Before building real features, remove the samples below and run \`akan sync <app>\`:
16094
+
16095
+ ${items.join(`
16096
+ `)}
16097
+
16098
+ Keep a sample only while you are still learning its pattern; delete it once your own modules cover the same ground.
16099
+
16100
+ `;
16035
16101
  };
16036
- var renderAgentRules = async (workspace, target) => {
16102
+ var renderManagedBlock = async (workspace) => {
16037
16103
  const context = await AkanContextAnalyzer.analyze(workspace);
16038
16104
  const frameworkGuide = await Prompter.getInstruction("framework");
16039
- return `# ${targetTitle[target]}
16040
-
16041
- This file is generated by \`akan agent install ${target}\`.
16042
-
16043
- ## Workspace
16105
+ const sampleCleanup = await renderSampleCleanup(workspace, context.apps.map((app) => app.name));
16106
+ return `## Workspace
16044
16107
 
16045
16108
  - Repo: ${context.repoName}
16046
16109
  - Apps: ${context.apps.map((app) => app.name).join(", ") || "none"}
16047
16110
  - Libraries: ${context.libs.map((lib) => lib.name).join(", ") || "none"}
16048
16111
  - Packages: ${context.pkgs.map((pkg) => pkg.name).join(", ") || "none"}
16049
16112
 
16050
- ## Akan Module Abstracts
16113
+ ${sampleCleanup}## Akan Module Abstracts
16051
16114
 
16052
16115
  - Before changing a domain, service, or scalar module, read its \`*.abstract.md\` file first.
16053
16116
  - Update the abstract when business invariants, workflows, or public behavior change.
@@ -16069,6 +16132,7 @@ If generated output is stale or broken, update the owning source file and run \`
16069
16132
  - After \`apply_workflow\`, run \`run_validation\` with \`validationTarget\` when present; otherwise use \`applyReportPath\`.
16070
16133
  - If no workflow exists, or apply reports unsupported/no-op/failed diagnostics that require manual action, keep edits scoped to owning source files and never patch generated files directly.
16071
16134
  - For compound requests, split the request into workflows and apply each \`planPath\` in order, such as \`create-module\` followed by \`add-field\`.
16135
+ - **CLI-only fallback (MCP not connected):** \`akan mcp\` starts a stdio MCP server, so the \`list_workflows\`/\`plan_workflow\`/\`apply_workflow\` tools exist only when your agent is wired to it as an MCP client. When they are unavailable, the CLI is a first-class equivalent: \`akan workflow list\` / \`explain <name>\` / \`plan <name> ... --format json --out <planPath>\` / \`apply <planPath> --format json\`, \`akan doctor --strict --format json\` for validation, and \`akan repair generated|imports|module-shape --app <app> --format json\` for repairs. Scaffolding primitives (\`create-module\`/\`create-scalar\`/\`create-service\` take the target app/lib as a POSITIONAL arg; \`add-field\`/\`add-enum-field\` use \`--app\`/\`--module\` flags) call the same code the workflows do.
16072
16136
 
16073
16137
  ## Validation
16074
16138
 
@@ -16077,8 +16141,61 @@ ${context.validationCommands.map((command3) => `- \`${command3}\``).join(`
16077
16141
 
16078
16142
  ## Framework Guide
16079
16143
 
16080
- ${frameworkGuide.trim()}
16144
+ ${frameworkGuide.trim()}`;
16145
+ };
16146
+ var upsertManagedBlock = (existing, block) => {
16147
+ const managed = `${AGENT_BLOCK_START}
16148
+ ${block}
16149
+ ${AGENT_BLOCK_END}`;
16150
+ const startIndex = existing.indexOf(AGENT_BLOCK_START);
16151
+ const endIndex = existing.indexOf(AGENT_BLOCK_END);
16152
+ if (startIndex >= 0 && endIndex > startIndex) {
16153
+ return `${existing.slice(0, startIndex)}${managed}${existing.slice(endIndex + AGENT_BLOCK_END.length)}`;
16154
+ }
16155
+ return `${existing.replace(/\s*$/, "")}
16156
+
16157
+ ${managed}
16158
+ `;
16159
+ };
16160
+ var renderAgentsMd = async (workspace, existing) => {
16161
+ const block = await renderManagedBlock(workspace);
16162
+ if (existing?.trim())
16163
+ return upsertManagedBlock(existing, block);
16164
+ const context = await AkanContextAnalyzer.analyze(workspace);
16165
+ return `# ${context.repoName} Agent Guide
16166
+
16167
+ This file is the single source of truth for coding agents in this workspace. Claude Code reads it through
16168
+ \`CLAUDE.md\` (\`@AGENTS.md\`) and Cursor through \`.cursor/rules/akan.mdc\`. The section between the
16169
+ \`akan:agent\` markers is regenerated by \`akan agent install\`; edit anything outside the markers freely.
16170
+
16171
+ ${AGENT_BLOCK_START}
16172
+ ${block}
16173
+ ${AGENT_BLOCK_END}
16174
+ `;
16175
+ };
16176
+ var renderClaudeMd = async (workspace) => {
16177
+ const context = await AkanContextAnalyzer.analyze(workspace);
16178
+ return `# ${context.repoName} \u2014 Claude Code Guide
16179
+
16180
+ @AGENTS.md
16181
+ `;
16182
+ };
16183
+ var renderCursorRule = () => `---
16184
+ description: Akan workspace agent guide
16185
+ alwaysApply: true
16186
+ ---
16187
+
16188
+ Follow the workspace agent guide, which is the single source of truth for Akan conventions,
16189
+ generated-file rules, and the MCP workflow policy.
16190
+
16191
+ @AGENTS.md
16081
16192
  `;
16193
+ var renderTarget = async (workspace, target, existing) => {
16194
+ if (target === "agents-md")
16195
+ return await renderAgentsMd(workspace, existing);
16196
+ if (target === "claude")
16197
+ return await renderClaudeMd(workspace);
16198
+ return renderCursorRule();
16082
16199
  };
16083
16200
 
16084
16201
  class AgentRunner extends runner("agent") {
@@ -16086,11 +16203,13 @@ class AgentRunner extends runner("agent") {
16086
16203
  const written = [];
16087
16204
  for (const target of targets) {
16088
16205
  const filePath = targetPaths[target];
16089
- if (!force && await workspace.exists(filePath)) {
16206
+ const exists2 = await workspace.exists(filePath);
16207
+ if (exists2 && !force && target !== "agents-md") {
16090
16208
  throw new Error(`${filePath} already exists. Re-run with --force to overwrite it.`);
16091
16209
  }
16092
- const content = await renderAgentRules(workspace, target);
16093
- await workspace.writeFile(filePath, content, { overwrite: force });
16210
+ const existing = exists2 ? await workspace.readFile(filePath) : null;
16211
+ const content = await renderTarget(workspace, target, existing);
16212
+ await workspace.writeFile(filePath, content, { overwrite: true });
16094
16213
  written.push(filePath);
16095
16214
  }
16096
16215
  return written;
@@ -21439,6 +21558,7 @@ import path48 from "path";
21439
21558
  var defaultWorkspacePeerDependencies = new Set([
21440
21559
  "@react-spring/web",
21441
21560
  "@use-gesture/react",
21561
+ "chance",
21442
21562
  "croner",
21443
21563
  "daisyui",
21444
21564
  "react",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akanjs/cli",
3
- "version": "2.3.11-rc.4",
3
+ "version": "2.3.11-rc.6",
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.4",
37
+ "akanjs": "2.3.11-rc.6",
38
38
  "chalk": "^5.6.2",
39
39
  "commander": "^14.0.3",
40
40
  "daisyui": "5.5.23",
@@ -4,12 +4,10 @@ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { a
4
4
  return {
5
5
  filename: "Task.Zone.tsx",
6
6
  content: `"use client";
7
- import { Task, usePage } from "@apps/${dict.appName}/client";
7
+ import { type cnst, Task, usePage } from "@apps/${dict.appName}/client";
8
8
  import type { ClientInit, ClientView } from "akanjs/fetch";
9
9
  import { Link, Load } from "akanjs/ui";
10
10
 
11
- import * as cnst from "../cnst";
12
-
13
11
  // ===== Task.Zone.tsx =====
14
12
  // Convention: lib/<module>/ — PascalCase .tsx, Zone suffix = composition layer between pages and UI.
15
13
  // Zone components use Load.Units / Load.View from akanjs/ui — the framework convention for data-bound zones.
@@ -10,14 +10,14 @@ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: Dic
10
10
  filename: "_index.tsx",
11
11
  content: `
12
12
  import { Load } from "akanjs/ui";
13
- import { fetch, usePage, ${dict.Model} } from "${dict.appName}/client";
13
+ import { fetch, usePage, ${dict.Model} } from "@apps/${dict.appName}/client";
14
14
  import type { PageConfig } from "akanjs/client";
15
15
 
16
16
  interface PageProps {
17
17
  params: { ${dict.model}Id: string };
18
18
  }
19
19
 
20
- export default function Page({ params }: PageProps) {
20
+ export default async function Page({ params }: PageProps) {
21
21
  const { l } = usePage();
22
22
  const { ${dict.model}Id } = params;
23
23
  const { ${dict.model}, ${dict.model}Edit } = await fetch.edit${dict.Model}(${dict.model}Id);
@@ -9,8 +9,8 @@ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: Dic
9
9
  return {
10
10
  filename: "_index.tsx",
11
11
  content: `
12
- import { ${dict.Model}, fetch, usePage } from "${dict.appName}/client";
13
- import { Link, Load } from "akanjs/ui";
12
+ import { ${dict.Model}, fetch, usePage } from "@apps/${dict.appName}/client";
13
+ import { Link } from "akanjs/ui";
14
14
  import type { PageConfig } from "akanjs/client";
15
15
 
16
16
  interface PageProps {
@@ -29,7 +29,7 @@ export async function generateHead({ params }: PageProps) {
29
29
  </>
30
30
  );
31
31
  }
32
- export default function Page({ params }: PageProps) {
32
+ export default async function Page({ params }: PageProps) {
33
33
  const { l } = usePage();
34
34
  const { ${dict.model}Id } = params;
35
35
  const { ${dict.model}, ${dict.model}View } = await fetch.view${dict.Model}(${dict.model}Id);
@@ -10,10 +10,10 @@ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: Dic
10
10
  filename: "_index.tsx",
11
11
  content: `
12
12
  import { ${dict.Model}, fetch, usePage } from "@apps/${dict.appName}/client";
13
- import { Link, Load } from "akanjs/ui";
13
+ import { Link } from "akanjs/ui";
14
14
  import type { PageConfig } from "akanjs/client";
15
15
 
16
- export default function Page() {
16
+ export default async function Page() {
17
17
  const { l } = usePage();
18
18
  const { ${dict.model}InitInPublic } = await fetch.init${dict.Model}InPublic();
19
19
  return (
@@ -10,10 +10,10 @@ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: Dic
10
10
  filename: "_index.tsx",
11
11
  content: `
12
12
  import type { PageConfig } from "akanjs/client";
13
- import { Load, Model } from "akanjs/ui";
14
- import { ${dict.Model}, type cnst, fetch, usePage } from "@apps/${dict.appName}/client";
13
+ import { Model } from "akanjs/ui";
14
+ import { ${dict.Model}, fetch, usePage } from "@apps/${dict.appName}/client";
15
15
 
16
- export default function Page() {
16
+ export default async function Page() {
17
17
  const { l } = usePage();
18
18
  const { ${dict.model}InitInPublic } = await fetch.init${dict.Model}InPublic();
19
19
  return (
@@ -11,7 +11,7 @@ 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 { cnst, ${dict.Model} } from "@${scanInfo?.type ?? "apps"}/${dict.sysName}/client";
14
+ import { type cnst, ${dict.Model} } from "@${scanInfo?.type ?? "apps"}/${dict.sysName}/client";
15
15
  import type { ClientInit, ClientView, SliceMeta } from "akanjs/fetch";
16
16
 
17
17
  interface CardProps {
@@ -1,38 +1,9 @@
1
1
  ---
2
- description: Akan.js workspace conventions for coding agents
2
+ description: Akan workspace agent guide
3
3
  alwaysApply: true
4
4
  ---
5
5
 
6
- # Akan.js Workspace Rules
6
+ Follow the workspace agent guide, which is the single source of truth for Akan conventions,
7
+ generated-file rules, and the MCP workflow policy.
7
8
 
8
- - Keep edits inside the established Akan structure: `apps/<app>`, `libs/<lib>`, `pkgs/*`, and `infra`.
9
- - Pages live under `apps/<app>/page`; index routes use `_index.tsx`, and nested layouts use `_layout.tsx`.
10
- - Database domain modules live under `lib/<model>`.
11
- - Service modules live under `lib/_<service>`.
12
- - Scalar modules live under `lib/__scalar/<scalar>`.
13
- - Module abstracts live beside module code as `<model>.abstract.md`, `<service>.abstract.md`, or
14
- `<scalar>.abstract.md`. Read them before changing module behavior.
15
- - Do not hand-edit generated Akan files such as `akan.app.json`, `client.ts`, `server.ts`, generated facet indexes,
16
- `lib/cnst.ts`, `lib/dict.ts`, `lib/db.ts`, `lib/srv.ts`, `lib/st.ts`, `lib/sig.ts`, `lib/useClient.ts`, or
17
- `lib/useServer.ts`.
18
- - Prefer Akan MCP workflows before direct source edits: use `akan mcp --mode plan` for workflow discovery and
19
- planning, then `akan mcp --mode apply` only for allowlisted apply, validation, and repair tools.
20
- - If `plan_workflow` returns `planPath` or `next.tool=apply_workflow`, call `apply_workflow({ planPath })` before
21
- editing source files directly.
22
- - After `apply_workflow`, run `run_validation` with `validationTarget` when present; otherwise use `applyReportPath`.
23
- - Direct source edits are denied when an allowlisted Akan workflow or repair tool can perform the change.
24
- - Direct edits are fallback only after `list_workflows`/`explain_workflow` show no matching workflow, or after
25
- `apply_workflow` reports unsupported/no-op/failed diagnostics that require manual action.
26
- - For compound requests, split the request into workflows and apply each `planPath` in order, such as `create-module`
27
- followed by `add-field`.
28
- - If generated output is stale or broken, update the owning source file and run `akan repair generated` or
29
- `akan sync <app-or-lib>` instead of patching generated files.
30
- - For new domain behavior, inspect sibling `constant`, `dictionary`, `signal`, `document`, `service`, `store`, and UI
31
- module files before changing shape.
32
- - Update `*.abstract.md` when business invariants, workflows, or public behavior change. Do not update it for
33
- formatting-only, import-only, or style-only changes.
34
- - Respect server/client import boundaries. Use `akanjs/server` only in server-side code and `akanjs/client` only in
35
- client/page runtime code.
36
- - Treat `AKAN_PUBLIC_*` env vars as public values. Do not store secrets in them.
37
- - Verify changes with the smallest relevant command: `akan lint <target>`, `akan test <target>`, or
38
- `akan build <app-name>`.
9
+ @AGENTS.md
@@ -87,7 +87,8 @@ When adding a new database-backed domain module (e.g., product, user):
87
87
 
88
88
  ```bash
89
89
  # 1. Scaffold the module with Akan CLI (creates constant, service, signal, store, document files)
90
- akan create-module <module-name> --app <%= appName %>
90
+ # The target app/lib is a POSITIONAL argument, not a --app flag.
91
+ akan create-module <module-name> <%= appName %>
91
92
 
92
93
  # 2. Start dev server with HMR and type checking at http://localhost:8282
93
94
  akan start <%= appName %>
@@ -114,12 +115,32 @@ akan build <%= appName %>
114
115
  ### Other Frequently Used Commands
115
116
 
116
117
  ```bash
117
- akan create-scalar <scalar-name> --app <%= appName %> # Add a scalar module (lib/__scalar/<scalar-name>/)
118
- akan create-service <service-name> --app <%= appName %> # Add a service module (lib/_<service-name>/)
118
+ akan create-scalar <scalar-name> <%= appName %> # Add a scalar module (lib/__scalar/<scalar-name>/)
119
+ akan create-service <service-name> <%= appName %> # Add a service module (lib/_<service-name>/)
119
120
  akan test <%= appName %> # Run the test code (lib/*/*.signal.test.ts or others)
120
121
  akan lint <%= appName %> # Lint only (no typecheck)
121
122
  ```
122
123
 
124
+ **CLI argument conventions.** Two argument styles, and mixing them up is a common mistake:
125
+
126
+ - Scaffolding and whole-app commands take the target app/lib as a **positional** argument, not a flag:
127
+
128
+ ```bash
129
+ akan create-module photo <%= appName %>
130
+ akan create-scalar money <%= appName %>
131
+ akan create-service billing <%= appName %>
132
+ akan sync <%= appName %>
133
+ ```
134
+
135
+ - Only the source-limited field commands use `--app`/`--module` flags:
136
+
137
+ ```bash
138
+ akan add-field --app <%= appName %> --module photo --field width --type Int
139
+ akan add-enum-field --app <%= appName %> --module photo --field status --values draft,active
140
+ ```
141
+
142
+ Passing `--app` to `create-module` is not recognized, and the target app will not resolve.
143
+
123
144
  For the default generated app, start with:
124
145
 
125
146
  ```bash
@@ -130,6 +151,11 @@ akan start <%= appName %>
130
151
 
131
152
  Almost every Akan.js change follows this pattern. **Missing sync or repair is the #1 cause of agent confusion.**
132
153
 
154
+ > **If the Akan MCP tools are not connected in your agent, skip straight to the CLI-only fallback below.**
155
+ > `akan mcp --mode plan/apply` starts a stdio MCP server that only works when your agent is wired to it as an
156
+ > MCP client. When those `list_workflows` / `plan_workflow` / `apply_workflow` tools are not available, the CLI
157
+ > commands are a fully supported, first-class path — you are not losing any capability by using them.
158
+
133
159
  1. **Plan** — Ask the Akan MCP server for the workflow first.
134
160
  ```
135
161
  akan mcp --mode plan
@@ -171,12 +197,31 @@ Almost every Akan.js change follows this pattern. **Missing sync or repair is th
171
197
 
172
198
  If `akan sync` gives errors, try:
173
199
  - `akan build <%= appName %>` — full rebuild catches type errors sync may miss
174
- - Re-run `akan create-module <name> --app <%= appName %>` if the scaffold is corrupted
200
+ - Re-run `akan create-module <name> <%= appName %>` if the scaffold is corrupted
175
201
 
176
202
  For compound natural-language requests, split the request into workflows and apply each artifact in order. For example,
177
203
  "create a project module and add a budget field" should run `create-module` plan/apply first, then `add-field`
178
204
  plan/apply, then validation/doctor on the returned `validationTarget`.
179
205
 
206
+ ### CLI-Only Fallback (MCP Not Connected)
207
+
208
+ When the Akan MCP tools are not loaded, run the CLI commands directly. Each MCP tool maps 1:1 to a CLI command,
209
+ and the CLI emits the same structured report via `--format json`:
210
+
211
+ | MCP tool | CLI-only equivalent |
212
+ |----------|---------------------|
213
+ | `list_workflows` | `akan workflow list` |
214
+ | `explain_workflow <name>` | `akan workflow explain <name>` |
215
+ | `plan_workflow <name> ...` | `akan workflow plan <name> ... --format json --out <planPath>` |
216
+ | `apply_workflow { planPath }` | `akan workflow apply <planPath> --format json` (add `--dry-run` to preview) |
217
+ | `run_validation { validationTarget }` | `akan doctor --strict --format json` (or `akan typecheck <%= appName %>`) |
218
+ | `repair_generated` / `repair_imports` / `repair_module_shape` | `akan repair generated\|imports\|module-shape --app <%= appName %> --format json` |
219
+
220
+ The scaffolding primitives (`akan create-module`, `akan create-scalar`, `akan create-service`, `akan add-field`,
221
+ `akan add-enum-field`) are the same primitives the workflows call, so `create-module <name> <%= appName %>` followed
222
+ by `akan sync <%= appName %>` is equivalent to running the `create-module` workflow. Direct source edits remain the
223
+ final fallback when no CLI command covers the change.
224
+
180
225
  ## Quick Decision Matrix — "Where do I put this code?"
181
226
 
182
227
  | You want to... | Create in... | Run after... |
@@ -289,7 +334,9 @@ const form = st.use.taskForm();
289
334
 
290
335
  ### Recipe 2: Injecting a Dependency into a Service
291
336
 
292
- Two patterns: injecting an **adapter** (external client) or injecting another **module's service**.
337
+ Three patterns: injecting an **external adapter** (`use<>()`), another **module's service** (`service<>()`),
338
+ or a **predefined framework adapter** (`plug()`). A field named `<refName>Service` resolves to the service
339
+ registered under `<refName>` — the `Service`/`Signal` suffix is required and stripped to derive the lookup key.
293
340
 
294
341
  **A. Adapter injection via `use<>()` (for external clients / global singletons)**
295
342
 
@@ -332,6 +379,32 @@ export class TaskService extends serve(db.task, ({ service }) => ({
332
379
  }
333
380
  ```
334
381
 
382
+ **C. Predefined framework adapter injection via `plug()` (storage, cache, queue, schedule, …)**
383
+
384
+ Akan ships predefined adapter roles from `akanjs/service`: `StorageAdaptorRole`, `CacheAdaptorRole`,
385
+ `QueueAdaptorRole`, `ScheduleAdaptorRole`, `DatabaseAdaptorRole`, `WebsocketAdaptorRole`,
386
+ `LoggingAdaptorRole`, `CompressAdaptorRole`. `plug()` injects the concrete adapter bound to that role (the
387
+ default `StorageAdaptor` binding is `BlobStorage`). `plug()` also accepts a concrete adapter class directly.
388
+
389
+ ```typescript
390
+ // In <model>.service.ts — inject the framework storage adapter by role
391
+ import { plug, serve, StorageAdaptorRole } from "akanjs/service";
392
+
393
+ export class TaskService extends serve(db.task, ({ plug }) => ({
394
+ storage: plug(StorageAdaptorRole),
395
+ })) {
396
+ async attach(taskId: string, path: string, localPath: string) {
397
+ // BlobStorage returns a URL under blobStorage.urlPrefix (default "/api/localFile/getBlob").
398
+ return await this.storage.uploadDataFromLocal({ path, localPath });
399
+ }
400
+ }
401
+ ```
402
+
403
+ For a custom adapter class (not a predefined role), pass the class itself, e.g. `ipfsApi: plug(IpfsApi)`
404
+ (see `libs/shared/lib/file/file.service.ts`). Injecting a file/image field is usually simpler than calling
405
+ storage directly: declare `image: field(File).optional()` (or `images: field([File])`) on the model and let the
406
+ store's generated `upload<Field>On<Model>(fileList)` action handle the upload.
407
+
335
408
  ---
336
409
 
337
410
  ### Recipe 3: Creating and Using a Slice
@@ -576,3 +649,13 @@ akan sync automatically generates APIs across all layers. Only write custom logi
576
649
  | `insight[Query](args)`, `query[Query](args)` | Insight and raw query |
577
650
 
578
651
  **Rule**: Define `Filter` with `.query()` conditions in `document.ts`. akan sync auto-generates all 10 query helper methods per filter. Write `Document` chain methods only for state transitions with validation.
652
+
653
+ ## Generated Context
654
+
655
+ The section below is regenerated by `akan agent install` (run `bun run setup:agent`). It lists this
656
+ workspace's apps, generated files, validation commands, and framework guide. Edit anything outside the
657
+ markers freely; content between them is overwritten on the next install.
658
+
659
+ <!-- akan:agent:start -->
660
+ <!-- Populated by `akan agent install`. Run `bun run setup:agent` to refresh. -->
661
+ <!-- akan:agent:end -->
@@ -50,7 +50,8 @@
50
50
  "!!**/dist",
51
51
  "!!**/.akan",
52
52
  "!!**/dump",
53
- "!!**/local"
53
+ "!!**/local",
54
+ "!!**/ios/DerivedData"
54
55
  ]
55
56
  },
56
57
  "formatter": {