@akanjs/devkit 3.1.1-rc.9 → 3.1.2-rc.0
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/CHANGELOG.md +117 -0
- package/agentAssets.test.ts +65 -0
- package/agentAssets.ts +166 -0
- package/agentsIndex.ts +4 -4
- package/akanApp/akanApp.host.test.ts +12 -0
- package/akanApp/akanApp.host.ts +12 -0
- package/akanConfig/akanConfig.test.ts +105 -0
- package/akanConfig/akanConfig.ts +21 -1
- package/akanConfig/productionLockfile.test.ts +31 -0
- package/akanConfig/productionLockfile.ts +2 -0
- package/akanContext.ts +129 -4
- package/akanMcpContract.test.ts +28 -1
- package/akanMcpContract.ts +51 -3
- package/applicationBuildRunner.test.ts +53 -0
- package/applicationBuildRunner.ts +7 -4
- package/artifact/implicitRootLayout.ts +4 -2
- package/biome.base.json +4 -1
- package/cloud/globalConfig.test.ts +34 -0
- package/cloud/globalConfig.ts +2 -2
- package/codeAgent/agent/AkanCodeEnv.ts +26 -0
- package/codeAgent/agent/AkanCodeServices.ts +5 -2
- package/codeAgent/agent/CodeAgent.ts +11 -0
- package/codeAgent/agent/CodeAgentGate.ts +156 -10
- package/codeAgent/agent/CodeAgentProxy.ts +5 -4
- package/codeAgent/agent/CodeAgentRpcHost.ts +3 -0
- package/codeAgent/agent/CodeAgentRpcListener.ts +102 -16
- package/codeAgent/agent/SubagentPool.ts +5 -0
- package/codeAgent/agent/WorkspaceTrust.ts +200 -0
- package/codeAgent/agent/akanCodePaths.ts +8 -1
- package/codeAgent/agent/akanSystemPrompt.ts +5 -1
- package/codeAgent/agent/codeAgent.test.ts +41 -2
- package/codeAgent/agent/codeAgentContract.test.ts +119 -0
- package/codeAgent/agent/codeAgentHosts.ts +30 -0
- package/codeAgent/agent/codeAgentProxy.test.ts +53 -1
- package/codeAgent/agent/codeAgentRpcListener.test.ts +115 -13
- package/codeAgent/agent/stringArg.ts +1 -1
- package/codeAgent/feedback/PreviewFeedback.ts +2 -2
- package/codeAgent/feedback/VerifyFeedback.ts +3 -4
- package/codeAgent/index.ts +2 -2
- package/codeAgent/skills/akan-agent/SKILL.md +43 -0
- package/codeAgent/skills/akan-code/SKILL.md +38 -11
- package/codeAgent/skills/akan-debug/SKILL.md +41 -6
- package/codeAgent/skills/akan-deps/SKILL.md +1 -2
- package/codeAgent/skills/akan-err/SKILL.md +2 -1
- package/codeAgent/skills/akan-mcp/SKILL.md +40 -0
- package/codeAgent/skills/akan-page/SKILL.md +44 -0
- package/codeAgent/skills/akan-query/SKILL.md +39 -0
- package/codeAgent/skills/akan-runtime/SKILL.md +43 -0
- package/codeAgent/skills/akan-scaffold/SKILL.md +16 -6
- package/codeAgent/skills/akan-transport/SKILL.md +40 -0
- package/codeAgent/skills/akan-validate/SKILL.md +13 -11
- package/codeAgent/tools/AkanCli.ts +2 -1
- package/codeAgent/tools/AkanCodePlugins.ts +5 -1
- package/codeAgent/tools/AkanEditScope.ts +8 -3
- package/codeAgent/tools/AkanToolPack.ts +4 -10
- package/codeAgent/tools/AkanVerifier.ts +13 -31
- package/codeAgent/tools/McpServerConfig.ts +6 -4
- package/codeAgent/tools/McpSignIn.ts +7 -7
- package/codeAgent/tools/McpTokenStore.ts +63 -16
- package/codeAgent/tools/McpToolPack.ts +8 -1
- package/codeAgent/tools/WebToolPack.ts +50 -6
- package/codeAgent/tools/akanCodePlugins.test.ts +130 -60
- package/codeAgent/tools/mcpOAuth.test.ts +50 -14
- package/codeAgent/tools/mcpServerConfig.test.ts +130 -1
- package/commandDecorators/command.ts +4 -1
- package/commandDecorators/workspaceVersionCheck.test.ts +17 -7
- package/commandDecorators/workspaceVersionCheck.ts +4 -1
- package/commentDensityScanner.test.ts +121 -0
- package/commentDensityScanner.ts +101 -0
- package/componentSizeScanner.test.ts +36 -0
- package/componentSizeScanner.ts +52 -0
- package/componentWriteChainScanner.test.ts +137 -0
- package/componentWriteChainScanner.ts +104 -0
- package/conventionSet.test.ts +73 -0
- package/conventionSet.ts +67 -0
- package/createTunnel.test.ts +200 -2
- package/createTunnel.ts +80 -6
- package/csrE2e/ssrRegistry.e2e.test.ts +6 -1
- package/deprecatedCommentScanner.test.ts +54 -0
- package/deprecatedCommentScanner.ts +30 -0
- package/endpointReach.test.ts +58 -0
- package/endpointReach.ts +80 -0
- package/envSeed.test.ts +90 -0
- package/envSeed.ts +68 -0
- package/executors.test.ts +19 -5
- package/executors.ts +59 -4
- package/fileSys.test.ts +30 -2
- package/fileSys.ts +14 -0
- package/formSetterScanner.test.ts +44 -1
- package/formSetterScanner.ts +53 -8
- package/frontendBuild/csrArtifactBuilder.ts +7 -0
- package/frontendBuild/mcpAppViewBuilder.test.ts +63 -0
- package/frontendBuild/mcpAppViewBuilder.ts +7 -1
- package/frontendBuild/optionalPeers.test.ts +132 -0
- package/index.ts +1 -0
- package/integration/devStability.integration.test.ts +9 -5
- package/integration/ssrMemoryProbe.ts +6 -0
- package/kindBranchScanner.test.ts +89 -0
- package/kindBranchScanner.ts +158 -0
- package/lint/__fixtures__/no-oversized-list-limit/bad.tsx +18 -0
- package/lint/__fixtures__/no-oversized-list-limit/good.tsx +23 -0
- package/lint/no-oversized-list-limit.grit +73 -0
- package/lint/no-unguarded-endpoint.grit +5 -5
- package/linter.test.ts +12 -0
- package/linter.ts +35 -12
- package/maintainabilityBaseline.test.ts +280 -0
- package/maintainabilityBaseline.ts +220 -0
- package/maintainabilityContract.test.ts +846 -0
- package/maintainabilityContract.ts +404 -0
- package/maintainabilityExceptionScanner.test.ts +90 -0
- package/maintainabilityExceptionScanner.ts +85 -0
- package/maintainabilityLimits.ts +60 -0
- package/maintainabilityReport.test.ts +130 -0
- package/maintainabilityReport.ts +153 -0
- package/mobile/desktopRelease.test.ts +1 -1
- package/mobile/desktopRelease.ts +3 -1
- package/mobile/desktopServerStage.test.ts +1 -0
- package/mobile/desktopServerStage.ts +3 -1
- package/mobile/nativeApp.test.ts +20 -5
- package/mobile/nativeApp.ts +4 -1
- package/mobile/nativeConfig.test.ts +209 -5
- package/mobile/nativeConfig.ts +127 -6
- package/mobile/nativePublicEnv.test.ts +2 -0
- package/mobile/nativePublicEnv.ts +13 -0
- package/package.json +4 -2
- package/platformTest/LinuxDockerTarget.ts +3 -0
- package/{codeAgent/feedback → preview}/PreviewChrome.ts +13 -7
- package/preview/PreviewView.ts +219 -0
- package/preview/index.ts +2 -0
- package/preview/previewView.test.ts +76 -0
- package/prompter.ts +7 -0
- package/qualityScanner.test.ts +75 -0
- package/qualityScanner.ts +17 -1
- package/routeSourceValidator.test.ts +11 -2
- package/routeSourceValidator.ts +14 -2
- package/serviceLoopCallScanner.test.ts +56 -0
- package/serviceLoopCallScanner.ts +122 -0
- package/shellOnlyEnv.test.ts +65 -0
- package/shellOnlyEnv.ts +54 -0
- package/signalMutationIndex.test.ts +15 -0
- package/signalMutationIndex.ts +23 -82
- package/sourceComments.test.ts +45 -0
- package/sourceComments.ts +70 -0
- package/sshHostKeys.ts +59 -0
- package/storeAccessScanner.test.ts +84 -0
- package/storeAccessScanner.ts +71 -0
- package/storeSource.ts +160 -0
- package/storeWriteScanner.test.ts +292 -0
- package/storeWriteScanner.ts +50 -0
- package/subspace.test.ts +10 -0
- package/subspaceClone.ts +3 -1
- package/subspaceConfig.ts +6 -0
- package/testOnlyExportScanner.test.ts +83 -0
- package/testOnlyExportScanner.ts +124 -0
- package/transforms/barrelImportsPlugin.ts +10 -4
- package/transforms/mcpAppPageTrimmer.test.ts +8 -2
- package/transforms/mcpAppPageTrimmer.ts +5 -20
- package/transforms/pureLazyMarker.ts +1 -1
- package/typeChecker.ts +32 -0
- package/typecheck/projectTypecheck.test.ts +18 -0
- package/typecheck/projectTypecheck.ts +29 -5
- package/typecheck/typecheck.proc.ts +6 -0
- package/typecheck/typecheckFailure.ts +13 -0
- package/types.ts +1 -0
- package/workflow/artifacts.ts +16 -2
- package/workflow/executor.ts +22 -7
- package/workflow/guardCatalog.test.ts +59 -0
- package/workflow/guardCatalog.ts +108 -0
- package/workflow/index.ts +4 -0
- package/workflow/interactionScaffold.ts +122 -0
- package/workflow/mutationScaffold.ts +533 -0
- package/workflow/mutationSource.test.ts +108 -0
- package/workflow/mutationSource.ts +219 -0
- package/workflow/plan.ts +82 -0
- package/workflow/render.ts +19 -4
- package/workflow/source.test.ts +185 -0
- package/workflow/source.ts +151 -24
- package/workflow/sourceAst.ts +10 -48
- package/workflow/types.ts +18 -1
- package/workspaceLayout.ts +2 -0
- package/writePath.test.ts +109 -0
- package/writePath.ts +219 -0
- package/writePlacementScanner.test.ts +77 -0
- package/writePlacementScanner.ts +50 -0
- package/writingActionIndex.test.ts +99 -0
- package/writingActionIndex.ts +63 -0
- package/codeAgent/feedback/PreviewView.ts +0 -95
- /package/{codeAgent/feedback → preview}/previewChrome.test.ts +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterAll, describe, expect, test } from "bun:test";
|
|
2
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
2
|
+
import { mkdtempSync, rmSync, statSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import type { CodeAgentEvent } from "akanjs/common";
|
|
@@ -44,23 +44,28 @@ class FakeAgent {
|
|
|
44
44
|
|
|
45
45
|
class Client {
|
|
46
46
|
lines: unknown[] = [];
|
|
47
|
+
readonly closed = Promise.withResolvers<void>();
|
|
47
48
|
#buffer = "";
|
|
48
49
|
#socket: Awaited<ReturnType<typeof Bun.connect>> | null = null;
|
|
49
|
-
static async connect(
|
|
50
|
+
static async connect(target: string | { port: number }) {
|
|
50
51
|
const client = new Client();
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
client.#buffer = parts.pop() ?? "";
|
|
58
|
-
for (const part of parts) if (part) client.lines.push(JSON.parse(part));
|
|
59
|
-
},
|
|
52
|
+
const socket = {
|
|
53
|
+
data: (_socket: unknown, data: Buffer) => {
|
|
54
|
+
client.#buffer += data.toString();
|
|
55
|
+
const parts = client.#buffer.split("\n");
|
|
56
|
+
client.#buffer = parts.pop() ?? "";
|
|
57
|
+
for (const part of parts) if (part) client.lines.push(JSON.parse(part));
|
|
60
58
|
},
|
|
61
|
-
|
|
59
|
+
close: () => client.closed.resolve(),
|
|
60
|
+
};
|
|
61
|
+
client.#socket = await (typeof target === "string"
|
|
62
|
+
? Bun.connect({ unix: target, socket })
|
|
63
|
+
: Bun.connect({ hostname: "127.0.0.1", port: target.port, socket }));
|
|
62
64
|
return client;
|
|
63
65
|
}
|
|
66
|
+
raw(text: string) {
|
|
67
|
+
this.#socket?.write(text);
|
|
68
|
+
}
|
|
64
69
|
send(id: string, command: object) {
|
|
65
70
|
this.#socket?.write(`${JSON.stringify({ id, command })}\n`);
|
|
66
71
|
}
|
|
@@ -81,10 +86,17 @@ describe("CodeAgentRpcListener", () => {
|
|
|
81
86
|
test("parses unix and tcp addresses, defaulting tcp to loopback", () => {
|
|
82
87
|
expect(CodeAgentRpcListener.parse("unix:/tmp/a.sock")).toEqual({ unix: "/tmp/a.sock" });
|
|
83
88
|
expect(CodeAgentRpcListener.parse("tcp:7000")).toEqual({ hostname: "127.0.0.1", port: 7000 });
|
|
84
|
-
expect(CodeAgentRpcListener.parse("
|
|
89
|
+
expect(CodeAgentRpcListener.parse("tcp::7000")).toEqual({ hostname: "127.0.0.1", port: 7000 });
|
|
90
|
+
expect(CodeAgentRpcListener.parse("localhost:7000")).toEqual({ hostname: "localhost", port: 7000 });
|
|
85
91
|
expect(() => CodeAgentRpcListener.parse("tcp:nope")).toThrow("--rpc-listen");
|
|
86
92
|
});
|
|
87
93
|
|
|
94
|
+
test("a bind other machines could reach is refused unless it is asked for", () => {
|
|
95
|
+
expect(() => CodeAgentRpcListener.parse("0.0.0.0:7000")).toThrow("--rpc-bind-any");
|
|
96
|
+
expect(() => CodeAgentRpcListener.parse("tcp:192.168.0.2:7000")).toThrow("--rpc-bind-any");
|
|
97
|
+
expect(CodeAgentRpcListener.parse("0.0.0.0:7000", { anyHost: true })).toEqual({ hostname: "0.0.0.0", port: 7000 });
|
|
98
|
+
});
|
|
99
|
+
|
|
88
100
|
test("the agent outlives a dropped client, and the next one catches up by seq", async () => {
|
|
89
101
|
const agent = new FakeAgent();
|
|
90
102
|
const unix = path.join(dir, "agent.sock");
|
|
@@ -110,4 +122,94 @@ describe("CodeAgentRpcListener", () => {
|
|
|
110
122
|
await served;
|
|
111
123
|
expect(agent.disposed).toBe(true);
|
|
112
124
|
});
|
|
125
|
+
|
|
126
|
+
describe("over tcp", () => {
|
|
127
|
+
const opened = () => {
|
|
128
|
+
const agent = new FakeAgent();
|
|
129
|
+
const listener = new CodeAgentRpcListener(new CodeAgentRpcHost(agent as unknown as CodeAgent, null), {
|
|
130
|
+
hostname: "127.0.0.1",
|
|
131
|
+
port: 0,
|
|
132
|
+
}).listen();
|
|
133
|
+
return { agent, listener, port: listener.port ?? 0 };
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
test("a connection opened with the token is answered, and one without it is closed unheard", async () => {
|
|
137
|
+
const { agent, listener, port } = opened();
|
|
138
|
+
try {
|
|
139
|
+
expect(listener.token).toMatch(/^[A-Za-z0-9_-]{43}$/);
|
|
140
|
+
const anonymous = await Client.connect({ port });
|
|
141
|
+
anonymous.send("1", { type: "prompt", message: "hi" });
|
|
142
|
+
await anonymous.closed.promise;
|
|
143
|
+
expect(anonymous.lines).toEqual([{ type: "reply", id: "auth", ok: false, error: "unauthorized" }]);
|
|
144
|
+
|
|
145
|
+
const wrong = await Client.connect({ port });
|
|
146
|
+
wrong.raw(`${JSON.stringify({ auth: "not-the-token" })}\n`);
|
|
147
|
+
await wrong.closed.promise;
|
|
148
|
+
expect(agent.seq).toBe(1);
|
|
149
|
+
|
|
150
|
+
const client = await Client.connect({ port });
|
|
151
|
+
client.raw(
|
|
152
|
+
`${JSON.stringify({ auth: listener.token })}\n${JSON.stringify({ id: "2", command: { type: "prompt", message: "hi" } })}\n`,
|
|
153
|
+
);
|
|
154
|
+
expect((await client.reply("auth")).ok).toBe(true);
|
|
155
|
+
expect((await client.reply("2")).ok).toBe(true);
|
|
156
|
+
expect(client.lines.findIndex((line) => (line as { id?: string }).id === "auth")).toBe(0);
|
|
157
|
+
client.close();
|
|
158
|
+
} finally {
|
|
159
|
+
listener.stop();
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("a connection that speaks anything but a JSON frame is dropped at its first byte", async () => {
|
|
164
|
+
const { listener, port } = opened();
|
|
165
|
+
try {
|
|
166
|
+
const browser = await Client.connect({ port });
|
|
167
|
+
browser.raw("GET / HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n");
|
|
168
|
+
await browser.closed.promise;
|
|
169
|
+
expect(browser.lines).toEqual([]);
|
|
170
|
+
} finally {
|
|
171
|
+
listener.stop();
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("a connection that has not authenticated does not displace the client that has", async () => {
|
|
176
|
+
const { listener, port } = opened();
|
|
177
|
+
try {
|
|
178
|
+
const client = await Client.connect({ port });
|
|
179
|
+
client.raw(`${JSON.stringify({ auth: listener.token })}\n`);
|
|
180
|
+
await client.reply("auth");
|
|
181
|
+
const other = await Client.connect({ port });
|
|
182
|
+
other.raw("{");
|
|
183
|
+
await Bun.sleep(30);
|
|
184
|
+
client.send("3", { type: "get_state", sinceSeq: 0 });
|
|
185
|
+
expect((await client.reply("3")).ok).toBe(true);
|
|
186
|
+
other.close();
|
|
187
|
+
client.close();
|
|
188
|
+
} finally {
|
|
189
|
+
listener.stop();
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test.skipIf(process.platform === "win32")(
|
|
195
|
+
"a unix socket is its owner's alone, and needs no token, though one is accepted",
|
|
196
|
+
async () => {
|
|
197
|
+
const agent = new FakeAgent();
|
|
198
|
+
const unix = path.join(dir, "owned.sock");
|
|
199
|
+
const listener = new CodeAgentRpcListener(new CodeAgentRpcHost(agent as unknown as CodeAgent, null), { unix });
|
|
200
|
+
listener.listen();
|
|
201
|
+
try {
|
|
202
|
+
expect(listener.token).toBeNull();
|
|
203
|
+
expect(statSync(unix).mode & 0o777).toBe(0o600);
|
|
204
|
+
const client = await Client.connect(unix);
|
|
205
|
+
client.raw(`${JSON.stringify({ auth: "anything" })}\n`);
|
|
206
|
+
expect((await client.reply("auth")).ok).toBe(true);
|
|
207
|
+
client.send("4", { type: "get_state", sinceSeq: 0 });
|
|
208
|
+
expect((await client.reply("4")).ok).toBe(true);
|
|
209
|
+
client.close();
|
|
210
|
+
} finally {
|
|
211
|
+
listener.stop();
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
);
|
|
113
215
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const stringArg = (args: unknown, key: "path" | "command") => {
|
|
1
|
+
export const stringArg = (args: unknown, key: "path" | "command" | "url") => {
|
|
2
2
|
if (!args || typeof args !== "object") return undefined;
|
|
3
3
|
const value = (args as Record<string, unknown>)[key];
|
|
4
4
|
return typeof value === "string" && value ? value : undefined;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { PreviewView } from "../../preview/PreviewView";
|
|
1
2
|
import { AkanEditScope } from "../tools/AkanEditScope";
|
|
2
|
-
import { PreviewView } from "./PreviewView";
|
|
3
3
|
import type { TurnFeedbackFinding, TurnFeedbackSource } from "./TurnFeedback";
|
|
4
4
|
|
|
5
5
|
export interface PreviewFeedbackOptions {
|
|
@@ -35,7 +35,7 @@ export class PreviewFeedback implements TurnFeedbackSource {
|
|
|
35
35
|
for (const route of this.#options.routes ?? ["/en/"]) {
|
|
36
36
|
const url = new URL(route, this.#options.previewUrl).toString();
|
|
37
37
|
const probe = await PreviewView.probe(url, { screenshot: !!this.#options.canSeeImages });
|
|
38
|
-
const problems = [...probe.pageErrors, ...probe.consoleErrors, ...probe.domFindings];
|
|
38
|
+
const problems = [...probe.pageErrors, ...probe.consoleErrors, ...probe.consoleWarnings, ...probe.domFindings];
|
|
39
39
|
if (!problems.length) continue;
|
|
40
40
|
findings.push(`**${url}**\n${problems.map((line) => `- ${line}`).join("\n")}`);
|
|
41
41
|
if (probe.screenshot) images.push({ data: probe.screenshot, mimeType: "image/png" });
|
|
@@ -26,11 +26,10 @@ export class VerifyFeedback implements TurnFeedbackSource {
|
|
|
26
26
|
|
|
27
27
|
async observe() {
|
|
28
28
|
const scope = await AkanEditScope.since(this.#options.cwd, this.#baseline);
|
|
29
|
-
if (!scope.apps.length && !scope.libs.length) return undefined;
|
|
29
|
+
if (!scope.apps.length && !scope.libs.length && !scope.pkgs.length) return undefined;
|
|
30
30
|
if (this.#options.readOnly) return VerifyFeedback.#readOnlyNotice(scope.needsSync, [...scope.apps, ...scope.libs]);
|
|
31
|
-
const report = await this.#verifier.verify(scope);
|
|
32
|
-
|
|
33
|
-
return AkanVerifier.summarize(report);
|
|
31
|
+
const report = await this.#verifier.verify(scope.paths);
|
|
32
|
+
return report.ok ? undefined : report.text;
|
|
34
33
|
}
|
|
35
34
|
|
|
36
35
|
static #readOnlyNotice(needsSync: boolean, targets: string[]) {
|
package/codeAgent/index.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Importing this barrel loads the engine (~122MiB resident); a host needing cheap pieces imports the owning module.
|
|
2
2
|
// agent/consoleToStderr is deliberately absent: it takes over globalThis.console, so an RPC entry imports it first.
|
|
3
|
+
|
|
4
|
+
export * from "../preview";
|
|
3
5
|
export * from "./agent/AkanCodeCompaction";
|
|
4
6
|
export * from "./agent/AkanCodeServices";
|
|
5
7
|
export * from "./agent/akanCodeModel";
|
|
@@ -17,9 +19,7 @@ export * from "./agent/CodeSessionFork";
|
|
|
17
19
|
export * from "./agent/CodeSessionIndex";
|
|
18
20
|
export * from "./agent/SubagentPool";
|
|
19
21
|
export * from "./feedback/DevLogFeedback";
|
|
20
|
-
export * from "./feedback/PreviewChrome";
|
|
21
22
|
export * from "./feedback/PreviewFeedback";
|
|
22
|
-
export * from "./feedback/PreviewView";
|
|
23
23
|
export * from "./feedback/TurnFeedback";
|
|
24
24
|
export * from "./feedback/VerifyFeedback";
|
|
25
25
|
export * from "./tools/AkanCli";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: akan-agent
|
|
3
|
+
description: "Build the in-page agent: mounting Agent.Chat, setAgentAccess and the request policy, the LLM, publishing st.tool actions and cards, forms, readable state, zones, route guidance and slash commands."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Akan in-page agent
|
|
7
|
+
|
|
8
|
+
**The surface is what components declare, exactly.** `st.tool` publishes one action and `st.use` / `st.sel` /
|
|
9
|
+
`st.ref` make one store key readable while the reading component is mounted. A method on a store class gives an
|
|
10
|
+
agent nothing, and only the current page's declarations are live.
|
|
11
|
+
|
|
12
|
+
## Mount it
|
|
13
|
+
|
|
14
|
+
1. Mount `<Agent.Chat />` once, in a layout. It never executes tools: every tool runs in the caller's browser,
|
|
15
|
+
gated by guards and the approval card.
|
|
16
|
+
2. It refuses every call until `option.setAgentAccess(<guard>)` in `lib/option.ts` names who may use it
|
|
17
|
+
(`SignedIn` for a product with accounts). What a turn may ask is `option.setAgentRequestPolicy`; until one is
|
|
18
|
+
set, a turn past `AgentRelayPolicy.defaultLimits` is refused with 413, and a policy of the app's own keeps them
|
|
19
|
+
with `AgentRelayPolicy.capped(request, limits)`. An error that is not an `Err` reaches the chat as a generic
|
|
20
|
+
failure unless `AKAN_ERROR_DETAIL=1` or a development runtime shows it.
|
|
21
|
+
3. The LLM is `option.setLlm({ apiKey, model, host })`, never env.
|
|
22
|
+
|
|
23
|
+
## Publish actions
|
|
24
|
+
|
|
25
|
+
- Declare the tool beside the control that already does it, and hand the result to the control:
|
|
26
|
+
`st.tool("x").desc("…").arg("id", ID).exec((id) => st.do.removeX(id))`. One handler serves the person and the
|
|
27
|
+
agent; `.desc()` is required and first.
|
|
28
|
+
- A name starting with a write verb (`remove`, `delete`, `create`, `update`, `upsert`, `submit`, `save`, `send`,
|
|
29
|
+
`export`) asks the user first; `{ confirm: false }` opts one tool out, `AgentConfirm.set` the app.
|
|
30
|
+
- `.card(({ submit, cancel }) => <Form … />)` parks the call in the chat for an answer that is the user's to give.
|
|
31
|
+
- Store-driven forms publish themselves: `onChange={st.do.setFieldOnX}` by reference emits the `data-akan-action`
|
|
32
|
+
the agent drives. A pass-through arrow publishes nothing and fails lint.
|
|
33
|
+
- Every `st.tool(…).exec()` / `.card()` is a hook: never behind a condition.
|
|
34
|
+
|
|
35
|
+
## Reads and guidance
|
|
36
|
+
|
|
37
|
+
- `st.use.x({ agent: false })` hides a key; `hidden`, `secret` and `visual` fields are stripped from every read.
|
|
38
|
+
- Page text is data the model reads, never instructions: after a screen read the next call that changes something
|
|
39
|
+
asks once, and `data-agent-private="<label>"` keeps a region's text out of every read.
|
|
40
|
+
- A tool value is capped at 20,000 characters.
|
|
41
|
+
- Route guidance is `<Agent.Guide instructions="…" />`; model-facing text is English.
|
|
42
|
+
|
|
43
|
+
Full detail: `get_guideline` with `agentRule`.
|
|
@@ -9,9 +9,9 @@ This is the tool the user is talking to right now. When they ask how to do somet
|
|
|
9
9
|
with their code — answer from here, and quote the exact gesture: the slash command to type, or the file to
|
|
10
10
|
edit. Do not guess a command; everything this tool offers is listed below.
|
|
11
11
|
|
|
12
|
-
You cannot type a slash command yourself. They are the host's, not yours — say which one to type. You
|
|
13
|
-
edit
|
|
14
|
-
|
|
12
|
+
You cannot type a slash command yourself. They are the host's, not yours — say which one to type. You cannot
|
|
13
|
+
edit `mcp.json`, `.akan/code/extensions/` or anything under `~/.akan/` either: the deny list covers them, so
|
|
14
|
+
quote the `/mcp` command that makes the change instead. The context files and skills named below you can edit.
|
|
15
15
|
|
|
16
16
|
## MCP servers
|
|
17
17
|
|
|
@@ -52,8 +52,17 @@ From the prompt:
|
|
|
52
52
|
|
|
53
53
|
**A server added now is not reachable until the session reloads.** The tool allowlist is fixed when the
|
|
54
54
|
session is created, so `add`, `remove`, `login` and `logout` all reload by themselves — a session that has
|
|
55
|
-
said nothing yet has nothing on disk to reopen, and says so.
|
|
56
|
-
|
|
55
|
+
said nothing yet has nothing on disk to reopen, and says so. After the user edits `mcp.json` by hand, `/mcp
|
|
56
|
+
reload` makes it take effect.
|
|
57
|
+
|
|
58
|
+
**This repo's own servers start only once the user approves them.** At start, `akan code` lists what this
|
|
59
|
+
repo's `.akan/code/mcp.json` would run, and the extensions in `.akan/code/extensions/`, and asks once; a yes is
|
|
60
|
+
remembered in `~/.akan/code/trust.json` until that content changes, and is asked again after. An extensions
|
|
61
|
+
folder past 10,000 files or 64 MiB, or with a link that leads out of it, is asked about at every start. With
|
|
62
|
+
nobody at a terminal they are not started, and `/mcp` shows `not approved`. `--trust-workspace` (`-T`) starts
|
|
63
|
+
them for one run without asking. Servers in `~/.akan/code/mcp.json`, where `/mcp add` writes, need no approval;
|
|
64
|
+
`/mcp add --local` writes this repo's file and keeps the approval it had, so a server added while that file
|
|
65
|
+
waits for approval waits with it.
|
|
57
66
|
|
|
58
67
|
`/mcp remove` on a name both files declare takes this repo's entry and leaves the home file's in force; its
|
|
59
68
|
notice says so, and running it again removes that one too.
|
|
@@ -61,8 +70,9 @@ notice says so, and running it again removes that one too.
|
|
|
61
70
|
Sign-in is discovered, not configured: the server's `401` names its resource metadata, that names the
|
|
62
71
|
authorization server, and most hosted servers register a client on demand — so a url is usually the whole
|
|
63
72
|
entry. For a provider that issues client ids by hand, add `"oauth": { "clientId": "…", "scope": "…" }`.
|
|
64
|
-
Tokens are stored
|
|
65
|
-
|
|
73
|
+
Tokens are stored in `~/.akan/code/mcpAuth.json` at mode 0600, never in the repo, bound to the server's origin
|
|
74
|
+
rather than its name — a server renamed in the file keeps its sign-in, and one moved to another url signs in
|
|
75
|
+
again. They are refreshed silently — at connect when one has expired, and mid-session when a server refuses one, after which the call is
|
|
66
76
|
retried once. When a refresh cannot fix it, `/mcp` shows `sign-in needed` and that server's tools answer with
|
|
67
77
|
`/mcp login <name>` until the user signs in; the sign-in reloads the session, which brings the server back.
|
|
68
78
|
|
|
@@ -85,19 +95,23 @@ Stored under `.akan/code/sessions` — project history, so a second checkout of
|
|
|
85
95
|
## Model, effort, keys
|
|
86
96
|
|
|
87
97
|
- `/model <provider>/<id>`, e.g. `/model deepseek/deepseek-v4-pro`. `/effort <level>` for how hard it thinks.
|
|
98
|
+
A `task` sub-agent starts on the model the session is on at that moment.
|
|
88
99
|
- A provider is authorized by **`<PROVIDER>_API_KEY`** — `DEEPSEEK_API_KEY`, `ANTHROPIC_API_KEY`. The
|
|
89
100
|
workspace's own `.env` is read as well as the environment, and a key found there is used for the run and
|
|
90
101
|
never copied anywhere.
|
|
91
|
-
- Credentials and the model catalogue live in `~/.akan/code/` (`auth.json`, `models.json`).
|
|
92
|
-
moves the whole directory** — for a container with no home, or a checkout that wants its
|
|
102
|
+
- Credentials and the model catalogue live in `~/.akan/code/` (`auth.json`, `models.json`), mode 0700.
|
|
103
|
+
**`AKAN_CODE_HOME` moves the whole directory** — for a container with no home, or a checkout that wants its
|
|
104
|
+
own. It, `AKAN_CODE_CAN_PROMPT`, the `AKAN_CODE_PROXY_*` settings and `AKAN_CODE_RPC_TOKEN_FILE` are read
|
|
105
|
+
from the shell only: one a repo's `.env` names is ignored, with a warning at start.
|
|
93
106
|
|
|
94
107
|
## What the agent reads, and how to change it
|
|
95
108
|
|
|
96
109
|
- Every `AGENTS.md` / `CLAUDE.md` from the workspace root down to the working directory, `.cursorrules`, and
|
|
97
110
|
any `.cursor/rules/*.mdc` that applies unconditionally (`alwaysApply: true`, or no frontmatter at all).
|
|
98
111
|
Edit those to change how this agent behaves in this repo — they are the files the editor beside it reads.
|
|
99
|
-
- Skills: drop a `<name>/SKILL.md` into `.akan/code/skills`. Extensions: `.akan/code/extensions
|
|
100
|
-
read from a home directory, so a session behaves the
|
|
112
|
+
- Skills: drop a `<name>/SKILL.md` into `.akan/code/skills`. Extensions: `.akan/code/extensions`, loaded once
|
|
113
|
+
the user approves them (see MCP servers). Neither is read from a home directory, so a session behaves the
|
|
114
|
+
same in two checkouts.
|
|
101
115
|
|
|
102
116
|
## Keys
|
|
103
117
|
|
|
@@ -123,8 +137,21 @@ akan code -i "<prompt>" open the session with the prompt already sent
|
|
|
123
137
|
--profile local|pod|review|web what it may do (default local)
|
|
124
138
|
--model <provider>/<id> --thinking, --json, --rpc, --resume <id>
|
|
125
139
|
--compact-at <percent> compact once the window is this full (10, 20, … 100)
|
|
140
|
+
--trust-workspace start this repo's MCP servers and extensions without asking
|
|
141
|
+
--no-ask-network never ask before an unlisted host (local profile)
|
|
142
|
+
--rpc-listen unix:<path>|tcp:[host:]<port> serve the RPC wire on a socket instead of stdio
|
|
126
143
|
```
|
|
127
144
|
|
|
145
|
+
`local` runs as the user: it refuses known secret files and the home credential stores, and asks before it
|
|
146
|
+
fetches a page from a host off a short documentation list or a shell command that reaches the network
|
|
147
|
+
(`curl`, `ssh`, …) — refused outright where nobody can answer, as in a one-shot `akan code "<prompt>"`. It is
|
|
148
|
+
not a sandbox; `pod` in a container is.
|
|
149
|
+
|
|
150
|
+
A `tcp:` listener asks every connection to open with `{"auth":"<token>"}` on its first line; the token is a
|
|
151
|
+
fresh one in `~/.akan/code/rpc/<port>.token`, or the one in the file `AKAN_CODE_RPC_TOKEN_FILE` names.
|
|
152
|
+
`--no-rpc-token` drops it, and only a loopback address is bound unless `--rpc-bind-any` is given. A unix
|
|
153
|
+
socket is created owner-only and asks for no token.
|
|
154
|
+
|
|
128
155
|
`review` reads and never writes, loads no project context files, and keeps no session on disk. `pod` has
|
|
129
156
|
nobody in front of it: it cannot ask a question. MCP is off in both.
|
|
130
157
|
|
|
@@ -1,10 +1,43 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: akan-debug
|
|
3
|
-
description: "
|
|
3
|
+
description: "Check and debug a running Akan app: akan preview for what a page shows and throws, where the dev-server log is, how to read it, and how to tell a runtime failure from a build one."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Akan debug
|
|
7
7
|
|
|
8
|
+
## See the change run: `akan preview`
|
|
9
|
+
|
|
10
|
+
`akan preview <app> [paths]` opens pages of the app's dev server in a headless browser and prints, per page,
|
|
11
|
+
the HTTP status, the title, a slice of the visible text, the uncaught errors and rejections the page reported,
|
|
12
|
+
the server errors logged meanwhile, console errors and warnings, and layout smells. It exits 1 when a page failed.
|
|
13
|
+
|
|
14
|
+
- It uses the dev server that answers for this app and checkout. With none, it starts one for the run and
|
|
15
|
+
stops it after; `--keep` leaves it up for the next preview. It never starts a second server beside a
|
|
16
|
+
running one.
|
|
17
|
+
- Paths are comma-separated (`akan preview <app> /board,/board/new`) and get the default locale when they lack
|
|
18
|
+
one, unless the app sets `localePrefix: "as-needed"` and `--csr` is off.
|
|
19
|
+
`--changed` adds the pages your uncommitted page and layout edits reach. A dynamic route needs a concrete
|
|
20
|
+
path (`/memo/1`); `akan context --app <app>` lists every route with its params and search names.
|
|
21
|
+
- The viewport is a 390x844 phone; `--desktop` is 1280x800. `--csr` loads each page in the CSR boot the native
|
|
22
|
+
shells load. `--screenshot` saves a PNG per page under `local/apps/<app>/runtime/preview/`, for a model
|
|
23
|
+
that reads images. `--format json` gives the same report as data.
|
|
24
|
+
|
|
25
|
+
### Which platforms that covers
|
|
26
|
+
|
|
27
|
+
- Every change: the server-rendered web page and, with `--csr`, the bundle the iOS, Android and desktop
|
|
28
|
+
shells run.
|
|
29
|
+
- A change to `native/`, `plugin/`, an `isNativeApp()` branch, `native` in `akan.config.ts` or an
|
|
30
|
+
`@akanjs/native` plugin also needs that shell: `akan start-ios`, `start-android` or `start-desktop` on top of
|
|
31
|
+
the running dev server. Errors thrown in the shell reach the same dev-server log as `ClientReport` lines
|
|
32
|
+
tagged with the platform and target. Capture the shell with `xcrun simctl io booted screenshot <file>.png` or
|
|
33
|
+
`adb exec-out screencap -p > <file>.png`. When you cannot run the shell, say so: "not checked on iOS".
|
|
34
|
+
- Real devices, memory, OTA updates and store builds are for whoever releases.
|
|
35
|
+
|
|
36
|
+
### What goes in your answer
|
|
37
|
+
|
|
38
|
+
The verdict lines of the pages you changed (`ok /en/board 200 "Board" 1.4s`) and every finding you left
|
|
39
|
+
unfixed. "Not opened in a browser" is not an answer once `akan preview` can run.
|
|
40
|
+
|
|
8
41
|
## The log is a file, and that file is the whole session
|
|
9
42
|
|
|
10
43
|
A supervised `akan start` writes **`local/apps/<app>/runtime/dev.log`** — no ANSI, nothing truncated, every
|
|
@@ -17,12 +50,14 @@ what survives in the scrollback is bordered and truncated to the pane.
|
|
|
17
50
|
- `tail -n 200 local/apps/<app>/runtime/dev.log` after an edit, to see what the reload produced.
|
|
18
51
|
- `akan logs <app>` tails a running server with `--level --grep --endpoint --trace --child --role --origin
|
|
19
52
|
--since` and `--json`.
|
|
20
|
-
- The dev server's URL is printed in that log
|
|
21
|
-
|
|
53
|
+
- The dev server's URL is printed in that log. A server `akan preview` started logs to
|
|
54
|
+
`local/apps/<app>/runtime/preview/server.log` instead.
|
|
55
|
+
- An uncaught error or unhandled rejection in a page reaches the server log as a `ClientReport` error line,
|
|
56
|
+
naming the page, the client (`ssr` or `csr`) and the native target.
|
|
22
57
|
|
|
23
58
|
## Which loop you are in
|
|
24
59
|
|
|
25
|
-
- **A type or lint failure** is the `akan-validate` loop
|
|
60
|
+
- **A type or lint failure** is the `akan-validate` loop: `akan verify`.
|
|
26
61
|
- **A runtime or SSR failure** is the dev log. The page renders blank, an overlay appears, or a request
|
|
27
62
|
500s — none of that shows up in a typecheck.
|
|
28
63
|
|
|
@@ -38,5 +73,5 @@ reads like its own level is not one.
|
|
|
38
73
|
|
|
39
74
|
## Before you conclude
|
|
40
75
|
|
|
41
|
-
A change that renders is not a change that passed. Run `
|
|
42
|
-
|
|
76
|
+
A change that renders is not a change that passed. Run `akan verify`; for a `.tsx` change it also runs the
|
|
77
|
+
SSR check, since a UI edit that moves markup into a client bundle typechecks perfectly.
|
|
@@ -46,5 +46,4 @@ the package client path — `import { cnst, fetch, st, usePage } from "@apps/<ap
|
|
|
46
46
|
|
|
47
47
|
## Validate
|
|
48
48
|
|
|
49
|
-
`
|
|
50
|
-
generated barrels and reports false errors.
|
|
49
|
+
`akan verify`. Raw `tsc` misses the generated barrels and reports false errors.
|
|
@@ -28,7 +28,8 @@ import path at all**, so keep throwing code out of them entirely rather than rea
|
|
|
28
28
|
|
|
29
29
|
Best-effort code returns a sentinel (`null`, `undefined`, `{}`) instead of throwing. `try/catch` is rare and
|
|
30
30
|
always converts an exception into a decision: a guard catches, warns, and returns `false`; an adapter
|
|
31
|
-
catches, logs, and returns `null`. A store action never catches — let the framework toast the `Err
|
|
31
|
+
catches, logs, and returns `null`. A store action never catches — let the framework toast the `Err`, or list it in
|
|
32
|
+
the store's `static silent` when no screen waits on it and its caller handles the rejection.
|
|
32
33
|
Client-side validation failure is `msg.error("<key>")` plus an early return, never a throw.
|
|
33
34
|
|
|
34
35
|
**A failure another process reported travels as itself.** A server-to-server `fetch.<endpoint>(…, { origin })`
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: akan-mcp
|
|
3
|
+
description: "Expose an app to MCP clients: what /mcp publishes from guards, mcp: false curation, option.setMcp settings, OAuth, refusals, page prompts and MCP Apps, and the exposure test."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Akan MCP
|
|
7
|
+
|
|
8
|
+
Every app serves its signals as an MCP server on `POST /mcp`, mounted by default. **Exposure follows the guards**:
|
|
9
|
+
an endpoint with a real guard is published, one with none is refused, and so is a mutation guarded only by
|
|
10
|
+
`Public`. There is no per-endpoint opt-in.
|
|
11
|
+
|
|
12
|
+
## Curation is not authorization
|
|
13
|
+
|
|
14
|
+
- `mcp: false` on an endpoint, or a `slice()` `mcp` map mirroring `guards`, keeps a perfectly guarded step (a UI
|
|
15
|
+
state-machine step a model would only call by mistake) off the shelf. It is an opt-out; everything else still
|
|
16
|
+
publishes.
|
|
17
|
+
- `guards: [Every, Person]` reserves an act for a person; `.with(AgentCall)` narrows what an agent's call may do.
|
|
18
|
+
- A guard's `static scope` decides whether a listing can hide the entry: `"account"` guards are evaluated per
|
|
19
|
+
caller, `"resource"` guards stay visible and stop the call.
|
|
20
|
+
|
|
21
|
+
## Settings
|
|
22
|
+
|
|
23
|
+
- `option.setMcp({ … })` in the app's `lib/option.ts`, never `main.ts`; `AKAN_MCP=false` takes the surface off.
|
|
24
|
+
- `JWT_SECRET` is required outside `local`.
|
|
25
|
+
- Every published entry needs a `.desc()`; model-facing text is English.
|
|
26
|
+
- Never make a refusal message more helpful.
|
|
27
|
+
|
|
28
|
+
## Pages for the host
|
|
29
|
+
|
|
30
|
+
- `page().prompt(name, description)` briefs a screen as an MCP prompt.
|
|
31
|
+
- `page().app(name, description, load)` publishes one component a host draws in the chat, as a read-only tool
|
|
32
|
+
whose `load` may only query.
|
|
33
|
+
|
|
34
|
+
## Verify
|
|
35
|
+
|
|
36
|
+
- One exposure test per app in `srvkit/mcpExposure.signal.test.ts`: list the tools with a user token, pin the names
|
|
37
|
+
with `toEqual`, and assert the curated and admin-only ones are absent. A guard change fails it first.
|
|
38
|
+
- Then `akan verify <app>` and the boot log.
|
|
39
|
+
|
|
40
|
+
Full detail: `get_guideline` with `mcpRule`, or `transportRule` for guards and rate limits.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: akan-page
|
|
3
|
+
description: "Add or change a route under page/: the one page()/layout() chain, typed params and search, guards, heads, loading and errors, overrides, and lib pages."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Akan pages
|
|
7
|
+
|
|
8
|
+
A route file exports one chain and nothing else. Every setting is a stage of it.
|
|
9
|
+
|
|
10
|
+
```tsx
|
|
11
|
+
export default page()
|
|
12
|
+
.param("orgId", ID)
|
|
13
|
+
.guard(() => signedIn())
|
|
14
|
+
.render(({ orgId }) => {
|
|
15
|
+
const { l } = usePage();
|
|
16
|
+
const { taskInitInOrg } = fetch.initTaskInOrg(orgId);
|
|
17
|
+
return <Task.Zone.Card init={taskInitInOrg} prefix={`/org/${orgId}`} />;
|
|
18
|
+
});
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Steps
|
|
22
|
+
|
|
23
|
+
1. Place the file: `<route>.tsx` or `<dir>/_index.tsx` serves the path, `_layout.tsx` nests a layout (`layout()`;
|
|
24
|
+
the root one is `rootLayout()`), `[id]` is a dynamic segment, `(group)` groups without a segment. `page/` holds
|
|
25
|
+
route modules only — no helpers, no `.ts` files.
|
|
26
|
+
2. Name every `[x]` segment with `.param("x", Type)` and read a query key only with `.search("k", Type)`. Values
|
|
27
|
+
arrive typed; `lang` reaches every stage and is never declared.
|
|
28
|
+
3. Gate with `.guard(() => signedIn())` on the `_layout.tsx` (a real redirect before anything renders), and repeat
|
|
29
|
+
a stricter guard on a page that needs more. The endpoint guards are still the real gate.
|
|
30
|
+
4. In `.render()`: `usePage()`, then fetch, then return. Hand `fetch.init*` / `view*` / `edit*` fields to sections
|
|
31
|
+
unawaited; await only what the shell reads, in one `Promise.all`.
|
|
32
|
+
5. `.head()` takes JSX (or a function returning it), never a metadata object. A canonical link comes only from the
|
|
33
|
+
page's own head.
|
|
34
|
+
6. Re-skin an `akanjs/ui` component for a subtree with `_overrides.tsx`:
|
|
35
|
+
`export default override({ Slot: AppComponent })`, no logic.
|
|
36
|
+
|
|
37
|
+
## Never
|
|
38
|
+
|
|
39
|
+
- A `fetch.<mutation>()` while rendering — it throws `base.error.mutationInRender`, because opening a link would
|
|
40
|
+
perform it. Send writes from a user action.
|
|
41
|
+
- `useState`, `useEffect`, comments, a named export beside the chain, or the legacy `export default async function`.
|
|
42
|
+
- Editing a lib page through its link in `page/(libs)/` — edit the lib's own `page/` source.
|
|
43
|
+
|
|
44
|
+
Full detail: `get_guideline` with `pageRule`, or `ssrRule` for what may render on the server.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: akan-query
|
|
3
|
+
description: "Write filters, slices and list loading: query descriptors, init paging and loadMoreOf, soft removal and hookless writes, q.search text search, and the cascade direction."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Akan queries and slices
|
|
7
|
+
|
|
8
|
+
## Filters and slices
|
|
9
|
+
|
|
10
|
+
- A filter in `<model>.document.ts` declares its args; a slice in `<model>.signal.ts` serves it to stores. A slice
|
|
11
|
+
`exec` returns a `query<Filter>(…)` descriptor — never a `list<Filter>` array — and you cannot chain sort or
|
|
12
|
+
limit on it.
|
|
13
|
+
- Page through the store: `initX(…, { sort, page, limit })`, then `st.do.loadMoreOf<Model>()` while
|
|
14
|
+
`hasMoreOf<Model>`. Never compare `count > page * limit`.
|
|
15
|
+
- Never ask for more than 500 rows: the server serves at most 500 per request and drops the rest silently.
|
|
16
|
+
A figure over the whole set belongs in an insight or an endpoint.
|
|
17
|
+
- The root slice takes a filter name and its args, never a raw query, and its `root:` guard is always `Admin`.
|
|
18
|
+
- No value is `q.empty`, never `q.missing`.
|
|
19
|
+
- A projection is bare on the facade (`pickById(id, { secret: true })`) and under `select` on a filter accessor —
|
|
20
|
+
the only way to read a `field.secret` value.
|
|
21
|
+
|
|
22
|
+
## Writes and removal
|
|
23
|
+
|
|
24
|
+
- Removal is soft: reads see live rows unless `{ withRemoved: true }`. The one hard delete is `purge<Filter>` /
|
|
25
|
+
`purgeMany`, for data past retention.
|
|
26
|
+
- A filter's `remove*` / `update*`, `updateById` and `removeById` run no hooks and no cascade. A model with a
|
|
27
|
+
cascade or `_postRemove` removes through `remove<Model>(id)`.
|
|
28
|
+
- Load a set once (`load<Model>Many`, one filter query) and create one with `create<Model>Many(rows)` instead of one
|
|
29
|
+
call per item in a loop — lint fails a per-item service call an endpoint reaches. `create<Model>Many` runs every
|
|
30
|
+
row's `_preCreate` before writing any, so a check there does not see the batch's own rows.
|
|
31
|
+
|
|
32
|
+
## Search and cascade
|
|
33
|
+
|
|
34
|
+
- `q.search(text, { prefix: true })` sits in a filter at an AND position, over fields with the `text` role. Blank
|
|
35
|
+
input matches nothing — never a passthrough. A search slice only over data safe to enumerate.
|
|
36
|
+
- `cascade` names a direction, and the wrong one loses data: `removeRef` on the owner's relation, `removeWith` on
|
|
37
|
+
the child's reference to its owner.
|
|
38
|
+
|
|
39
|
+
Full detail: `get_guideline` with `queryRule`, and `workspaceRecipes` for a worked slice.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: akan-runtime
|
|
3
|
+
description: "Configure how an app runs: web surfaces, route prefixes, gateway vs solo, akan start and logs, the Docker image, native and desktop apps, database modes, ops, assets and the build pipeline."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Akan runtime
|
|
7
|
+
|
|
8
|
+
Runtime and build settings are declared in `akan.config.ts` and narrowed — never widened — by an env at boot.
|
|
9
|
+
Domain code reaches none of them.
|
|
10
|
+
|
|
11
|
+
## Where each setting lives
|
|
12
|
+
|
|
13
|
+
| Want | Setting |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
| API only, or no CSR bundle for native apps | `web: false`, `web: { csr: false }` |
|
|
16
|
+
| Move the API or websocket mount | `new AkanApp({ prefix, websocketPrefix })` in `main.ts`, mirrored in `api: { … }`; read with `getApiPrefix()` / `getWsPrefix()` |
|
|
17
|
+
| The production image | `docker: string \| { image, preRuns, postRuns, command }` |
|
|
18
|
+
| iOS, Android, desktop | the `native` section; `desktop: { server: true }` carries the server |
|
|
19
|
+
| SQLite, several databases, a cluster | `database: { modes: [...] }`, picked by `AKAN_DATABASE_MODE` |
|
|
20
|
+
| URLs without `/<locale>/`: one language, or paths fixed from outside (OAuth callback, QR code, mail link) | `i18n: { localePrefix: "as-needed" }` — the default locale's pages and `<Link>` hrefs drop the prefix, other locales keep it |
|
|
21
|
+
|
|
22
|
+
- `main.ts` imports `AkanApp` from `akanjs/server/akanApp`, never the barrel.
|
|
23
|
+
- Never write `/api` or `/ws` literally, and never pass a prefix as a prop.
|
|
24
|
+
- The generated image installs only `ca-certificates` and `tzdata`; add the rest in `preRuns`.
|
|
25
|
+
- An app running in two database modes avoids `q.raw`.
|
|
26
|
+
|
|
27
|
+
## Running and logs
|
|
28
|
+
|
|
29
|
+
- `akan start <app>` (`a,b` or `all` for several) runs the dev server; `akan build <app>` checks the production
|
|
30
|
+
build. A supervised session writes `local/apps/<app>/runtime/dev.log` — hand over that path, not a paste of the
|
|
31
|
+
live view. Tail with `akan logs <app>`.
|
|
32
|
+
- Run a build without Docker with `cd dist/apps/<app> && bun main.js`: it boots with the env the Dockerfile sets,
|
|
33
|
+
read from `akan.env.json` beside it, and a variable you set yourself (`PORT=3000`) wins.
|
|
34
|
+
- Give every log sink a floor (`Logger.addSink(sink, { minLevel })`) and never log once per delivered record.
|
|
35
|
+
|
|
36
|
+
## Native and desktop
|
|
37
|
+
|
|
38
|
+
- `akan start-desktop <app>`, `akan start-ios <app>`, `akan start-android <app>` load pages from a running
|
|
39
|
+
`akan start`; a desktop app carrying its server starts it itself.
|
|
40
|
+
- A desktop app's carried server listens on a loopback port any local program can call — guard it like a network
|
|
41
|
+
server. Read runtime files from `AKAN_APP_DIR`, and keep a user's secret with `NativeSecret`, never in a file.
|
|
42
|
+
|
|
43
|
+
Full detail: `get_guideline` with `runtimeRule`.
|