@docyrus/docyrus 0.0.42 → 0.0.43

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docyrus/docyrus",
3
- "version": "0.0.42",
3
+ "version": "0.0.43",
4
4
  "private": false,
5
5
  "description": "Docyrus API CLI",
6
6
  "main": "./main.js",
@@ -746,8 +746,8 @@ async function architectHandler(pi: ExtensionAPI, ctx: ExtensionCommandContext,
746
746
  }
747
747
 
748
748
  export default function architectExtension(pi: ExtensionAPI) {
749
- pi.registerCommand("architect", {
750
- description: "Analyze tenant data sources and plan Docyrus schema artifacts for an app idea",
749
+ pi.registerCommand("plan", {
750
+ description: "Discover tenant state and start a planning-only branch for an app idea or task",
751
751
  handler: async(args, ctx) => {
752
752
  await architectHandler(pi, ctx, args);
753
753
  },
@@ -2,16 +2,6 @@
2
2
  "name": "pi-mcp-adapter",
3
3
  "version": "2.2.0",
4
4
  "description": "MCP (Model Context Protocol) adapter extension for Pi coding agent",
5
- "type": "module",
6
- "license": "MIT",
7
- "author": "Nico Bailon",
8
- "bin": {
9
- "pi-mcp-adapter": "./cli.js"
10
- },
11
- "repository": {
12
- "type": "git",
13
- "url": "https://github.com/nicobailon/pi-mcp-adapter"
14
- },
15
5
  "keywords": [
16
6
  "pi-package",
17
7
  "pi",
@@ -23,16 +13,15 @@
23
13
  "claude",
24
14
  "llm"
25
15
  ],
26
- "scripts": {
27
- "test": "vitest run",
28
- "test:watch": "vitest",
29
- "test:coverage": "vitest run --coverage"
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/nicobailon/pi-mcp-adapter"
30
19
  },
31
- "pi": {
32
- "extensions": [
33
- "./index.ts"
34
- ],
35
- "video": "https://github.com/nicobailon/pi-mcp-adapter/raw/refs/heads/main/pi-mcp.mp4"
20
+ "license": "MIT",
21
+ "author": "Nico Bailon",
22
+ "type": "module",
23
+ "bin": {
24
+ "pi-mcp-adapter": "./cli.js"
36
25
  },
37
26
  "files": [
38
27
  "cli.js",
@@ -68,18 +57,29 @@
68
57
  "CHANGELOG.md",
69
58
  "LICENSE"
70
59
  ],
60
+ "scripts": {
61
+ "test": "vitest run",
62
+ "test:coverage": "vitest run --coverage",
63
+ "test:watch": "vitest"
64
+ },
71
65
  "dependencies": {
72
66
  "@modelcontextprotocol/ext-apps": "^1.2.2",
73
67
  "@modelcontextprotocol/sdk": "^1.25.1",
74
68
  "@sinclair/typebox": "^0.32.0",
75
69
  "zod": "^3.25.0 || ^4.0.0"
76
70
  },
77
- "peerDependencies": {
78
- "zod": "^3.25.0 || ^4.0.0"
79
- },
80
71
  "devDependencies": {
81
72
  "@types/node": "^20.0.0",
82
73
  "typescript": "^5.0.0",
83
74
  "vitest": "^3.0.0"
75
+ },
76
+ "peerDependencies": {
77
+ "zod": "^3.25.0 || ^4.0.0"
78
+ },
79
+ "pi": {
80
+ "extensions": [
81
+ "./index.ts"
82
+ ],
83
+ "video": "https://github.com/nicobailon/pi-mcp-adapter/raw/refs/heads/main/pi-mcp.mp4"
84
84
  }
85
85
  }
@@ -511,7 +511,7 @@ function buildPlanPromptOverlay(state: IPlanSessionState): string {
511
511
  const task = state.task?.trim() || "(unknown task)";
512
512
  const model = state.planModel ? `Planning model: ${state.planModel}` : "Planning model: current session model";
513
513
  const artifact = state.artifactPath ? `Artifact path: ${state.artifactPath}` : "Artifact path: unavailable";
514
- const modeLabel = state.mode === "architect" ? "Docyrus `/architect` planning mode." : "Docyrus `/plan` mode.";
514
+ const modeLabel = "Docyrus `/plan` mode.";
515
515
  return [
516
516
  modeLabel,
517
517
  PLAN_MODE_SYSTEM_PROMPT,
@@ -735,7 +735,115 @@ export function isPotentiallyMutatingShellCommand(command: string): boolean {
735
735
  }
736
736
 
737
737
  function buildBlockedToolReason(detail: string): string {
738
- return `A planning session is active. ${detail} Exit with /end-plan or /end-architect before implementing.`;
738
+ return `A planning session is active. ${detail} Exit with /end-plan before implementing.`;
739
+ }
740
+
741
+ const READONLY_STATE_TYPE = "readonly-session";
742
+ const READONLY_WIDGET_KEY = "readonly-mode";
743
+
744
+ interface IReadOnlySessionState {
745
+ active: boolean;
746
+ startedAt?: string;
747
+ }
748
+
749
+ let currentReadOnlyState: IReadOnlySessionState | undefined;
750
+
751
+ const READONLY_MODE_SYSTEM_PROMPT = [
752
+ "You are in Docyrus `/read-only` mode.",
753
+ "",
754
+ "You MUST NOT modify any files or remote state.",
755
+ "You can read files, search code, analyze, summarize, and answer questions.",
756
+ "Write and edit tools are disabled.",
757
+ "",
758
+ "Exit read-only mode with /end-read-only to resume normal operation.",
759
+ ].join("\n");
760
+
761
+ function getReadOnlyState(ctx: ExtensionContext): IReadOnlySessionState | undefined {
762
+ let state: IReadOnlySessionState | undefined;
763
+ for (const entry of ctx.sessionManager.getBranch()) {
764
+ if (entry.type === "custom" && entry.customType === READONLY_STATE_TYPE) {
765
+ state = entry.data as IReadOnlySessionState | undefined;
766
+ }
767
+ }
768
+
769
+ if (state?.active) {
770
+ return state;
771
+ }
772
+
773
+ return undefined;
774
+ }
775
+
776
+ function setReadOnlyWidget(ctx: ExtensionContext, state: IReadOnlySessionState | undefined): void {
777
+ if (!ctx.hasUI) {
778
+ return;
779
+ }
780
+
781
+ if (!state?.active) {
782
+ ctx.ui.setWidget(READONLY_WIDGET_KEY, undefined);
783
+ return;
784
+ }
785
+
786
+ ctx.ui.setWidget(READONLY_WIDGET_KEY, (_tui, theme) => {
787
+ const lines = [
788
+ theme.fg("accent", theme.bold("Read-only mode active")),
789
+ theme.fg("muted", "Write & edit tools disabled"),
790
+ theme.fg("warning", "Exit with /end-read-only"),
791
+ ];
792
+
793
+ const container = new Container();
794
+ container.addChild(new DynamicBorder((value: string) => theme.fg("accent", value)));
795
+ container.addChild(new Text(lines.join("\n"), 0, 0));
796
+ return container;
797
+ });
798
+ }
799
+
800
+ function buildBlockedReadOnlyReason(detail: string): string {
801
+ return `Read-only mode is active. ${detail} Exit with /end-read-only to resume editing.`;
802
+ }
803
+
804
+ function handleToolCallDuringReadOnly(event: ToolCallEvent, ctx: ExtensionContext): { block: boolean; reason: string } | undefined {
805
+ if (!getReadOnlyState(ctx)?.active) {
806
+ return undefined;
807
+ }
808
+
809
+ if (event.toolName === "edit" || event.toolName === "write") {
810
+ return {
811
+ block: true,
812
+ reason: buildBlockedReadOnlyReason(`The ${event.toolName} tool is disabled in read-only mode.`),
813
+ };
814
+ }
815
+
816
+ if (event.toolName === "bash" && isRecord(event.input) && typeof event.input.command === "string") {
817
+ if (isPotentiallyMutatingShellCommand(event.input.command)) {
818
+ return {
819
+ block: true,
820
+ reason: buildBlockedReadOnlyReason(`Mutating bash commands are disabled in read-only mode (${event.input.command}).`),
821
+ };
822
+ }
823
+ }
824
+
825
+ if (event.toolName === "todo" && isRecord(event.input) && typeof event.input.action === "string") {
826
+ if (shouldBlockTodoAction(event.input.action)) {
827
+ return {
828
+ block: true,
829
+ reason: buildBlockedReadOnlyReason(`Todo action "${event.input.action}" is disabled in read-only mode.`),
830
+ };
831
+ }
832
+ }
833
+
834
+ return undefined;
835
+ }
836
+
837
+ function handleUserBashDuringReadOnly(event: UserBashEvent, ctx: ExtensionContext) {
838
+ if (!getReadOnlyState(ctx)?.active) {
839
+ return undefined;
840
+ }
841
+
842
+ if (!isPotentiallyMutatingShellCommand(event.command)) {
843
+ return undefined;
844
+ }
845
+
846
+ return buildBlockedUserBashResult(`Mutating shell commands are disabled in read-only mode (${event.command}).`);
739
847
  }
740
848
 
741
849
  function buildBlockedUserBashResult(detail: string) {
@@ -900,7 +1008,11 @@ export async function startPlanningWorkflow(params: IPlanningStartParams): Promi
900
1008
  }
901
1009
 
902
1010
  try {
903
- await ensureArtifactFile(artifactPath, params.task ?? mode);
1011
+ if (mode === "architect") {
1012
+ await fs.mkdir(artifactPath, { recursive: true });
1013
+ } else {
1014
+ await ensureArtifactFile(artifactPath, params.task ?? mode);
1015
+ }
904
1016
  } catch (error) {
905
1017
  if (ctx.hasUI) {
906
1018
  const message = error instanceof Error ? error.message : String(error);
@@ -968,32 +1080,13 @@ export async function startPlanningWorkflow(params: IPlanningStartParams): Promi
968
1080
 
969
1081
  if (ctx.hasUI) {
970
1082
  const profileHint = profileName ? ` (profile: ${profileName})` : "";
971
- const modeLabel = mode === "architect" ? "Architect mode" : "Plan mode";
972
- ctx.ui.notify(`${modeLabel} active${profileHint}. Writing updates to ${artifactPath}`, "info");
1083
+ ctx.ui.notify(`Plan mode active${profileHint}. Writing updates to ${artifactPath}`, "info");
973
1084
  }
974
1085
 
975
1086
  pi.sendUserMessage(initialPrompt);
976
1087
  return true;
977
1088
  }
978
1089
 
979
- async function planHandler(pi: ExtensionAPI, ctx: ExtensionCommandContext, args: string): Promise<void> {
980
- const task = parsePlanTask(args) ?? undefined;
981
- const artifactPath = createPlanArtifactPath({
982
- cwd: ctx.cwd,
983
- task: task ?? "plan",
984
- date: new Date(),
985
- });
986
-
987
- await startPlanningWorkflow({
988
- pi,
989
- ctx,
990
- mode: "plan",
991
- task,
992
- artifactPath,
993
- initialPrompt: buildInitialPlanPrompt(task),
994
- });
995
- }
996
-
997
1090
  export async function endPlanningWorkflow(pi: ExtensionAPI, ctx: ExtensionCommandContext): Promise<void> {
998
1091
  const state = getPlanState(ctx);
999
1092
  if (!state?.active || !state.originId) {
@@ -1208,13 +1301,6 @@ function handleUserBashDuringPlan(event: UserBashEvent, ctx: ExtensionContext) {
1208
1301
  }
1209
1302
 
1210
1303
  export default function planExtension(pi: ExtensionAPI) {
1211
- pi.registerCommand("plan", {
1212
- description: "Start a planning-only branch with an optional dedicated planning model",
1213
- handler: async(args, ctx) => {
1214
- await planHandler(pi, ctx, args);
1215
- },
1216
- });
1217
-
1218
1304
  pi.registerCommand("end-plan", {
1219
1305
  description: "Leave plan mode, summarize the plan branch, and return to the original branch",
1220
1306
  handler: async(_args, ctx) => {
@@ -1222,37 +1308,85 @@ export default function planExtension(pi: ExtensionAPI) {
1222
1308
  },
1223
1309
  });
1224
1310
 
1225
- pi.registerCommand("end-architect", {
1226
- description: "Leave architect mode, summarize the architect branch, and return to the original branch",
1311
+ pi.registerCommand("plan-policy", {
1312
+ description: "Show effective planning-model policy and the resolved planning model",
1313
+ handler: async(_args, ctx) => {
1314
+ await planPolicyHandler(pi, ctx);
1315
+ },
1316
+ });
1317
+
1318
+ pi.registerCommand("read-only", {
1319
+ description: "Enter read-only mode — write and edit tools are disabled, analysis and search remain available",
1227
1320
  handler: async(_args, ctx) => {
1228
- await endPlanningWorkflow(pi, ctx);
1321
+ if (getReadOnlyState(ctx)?.active) {
1322
+ if (ctx.hasUI) {
1323
+ ctx.ui.notify("Read-only mode is already active. Exit with /end-read-only first.", "warning");
1324
+ }
1325
+ return;
1326
+ }
1327
+
1328
+ const state: IReadOnlySessionState = {
1329
+ active: true,
1330
+ startedAt: new Date().toISOString(),
1331
+ };
1332
+ pi.appendEntry(READONLY_STATE_TYPE, state);
1333
+ currentReadOnlyState = state;
1334
+ setReadOnlyWidget(ctx, state);
1335
+
1336
+ if (ctx.hasUI) {
1337
+ ctx.ui.notify("Read-only mode active. Write & edit tools disabled. Exit with /end-read-only.", "info");
1338
+ }
1229
1339
  },
1230
1340
  });
1231
1341
 
1232
- pi.registerCommand("plan-policy", {
1233
- description: "Show effective planning-model policy and the resolved planning model",
1342
+ pi.registerCommand("end-read-only", {
1343
+ description: "Exit read-only mode and resume normal operation",
1234
1344
  handler: async(_args, ctx) => {
1235
- await planPolicyHandler(pi, ctx);
1345
+ if (!getReadOnlyState(ctx)?.active) {
1346
+ if (ctx.hasUI) {
1347
+ ctx.ui.notify("No active read-only session was found.", "info");
1348
+ }
1349
+ return;
1350
+ }
1351
+
1352
+ pi.appendEntry(READONLY_STATE_TYPE, { active: false });
1353
+ currentReadOnlyState = undefined;
1354
+ setReadOnlyWidget(ctx, undefined);
1355
+
1356
+ if (ctx.hasUI) {
1357
+ ctx.ui.notify("Read-only mode ended. Normal operation resumed.", "info");
1358
+ }
1236
1359
  },
1237
1360
  });
1238
1361
 
1239
1362
  pi.on("before_agent_start", (event, ctx) => {
1240
- const state = getPlanState(ctx);
1241
- if (!state?.active) {
1363
+ const planState = getPlanState(ctx);
1364
+ const readOnlyState = getReadOnlyState(ctx);
1365
+ const overlays: string[] = [];
1366
+
1367
+ if (planState?.active) {
1368
+ overlays.push(buildPlanPromptOverlay(planState));
1369
+ }
1370
+
1371
+ if (readOnlyState?.active) {
1372
+ overlays.push(READONLY_MODE_SYSTEM_PROMPT);
1373
+ }
1374
+
1375
+ if (overlays.length === 0) {
1242
1376
  return;
1243
1377
  }
1244
1378
 
1245
1379
  return {
1246
- systemPrompt: [event.systemPrompt, buildPlanPromptOverlay(state)].filter(Boolean).join("\n\n"),
1380
+ systemPrompt: [event.systemPrompt, ...overlays].filter(Boolean).join("\n\n"),
1247
1381
  };
1248
1382
  });
1249
1383
 
1250
1384
  pi.on("tool_call", (event, ctx) => {
1251
- return handleToolCallDuringPlan(event, ctx);
1385
+ return handleToolCallDuringPlan(event, ctx) ?? handleToolCallDuringReadOnly(event, ctx);
1252
1386
  });
1253
1387
 
1254
1388
  pi.on("user_bash", (event, ctx) => {
1255
- return handleUserBashDuringPlan(event, ctx);
1389
+ return handleUserBashDuringPlan(event, ctx) ?? handleUserBashDuringReadOnly(event, ctx);
1256
1390
  });
1257
1391
 
1258
1392
  pi.on("agent_end", async(event, ctx) => {
@@ -1275,14 +1409,22 @@ export default function planExtension(pi: ExtensionAPI) {
1275
1409
 
1276
1410
  pi.on("session_start", async(_event, ctx) => {
1277
1411
  await syncPlanState(pi, ctx);
1412
+ const readOnlyState = getReadOnlyState(ctx);
1413
+ currentReadOnlyState = readOnlyState;
1414
+ setReadOnlyWidget(ctx, readOnlyState);
1278
1415
  });
1279
1416
 
1280
1417
  pi.on("session_tree", async(_event, ctx) => {
1281
1418
  await syncPlanState(pi, ctx);
1419
+ const readOnlyState = getReadOnlyState(ctx);
1420
+ currentReadOnlyState = readOnlyState;
1421
+ setReadOnlyWidget(ctx, readOnlyState);
1282
1422
  });
1283
1423
 
1284
1424
  pi.on("session_shutdown", async(_event, ctx) => {
1285
1425
  currentPlanState = undefined;
1286
1426
  setPlanWidget(ctx, undefined);
1427
+ currentReadOnlyState = undefined;
1428
+ setReadOnlyWidget(ctx, undefined);
1287
1429
  });
1288
1430
  }
@@ -9,6 +9,7 @@ Core behavior:
9
9
  - Start by inspecting real tenant state before making claims about apps, data sources, users, environments, auth state, or API shape.
10
10
  - If the repository contains `docyrus/knowledge/`, use `docyrus knowledge search`, `docyrus knowledge section`, and `docyrus knowledge expand` before broader repo exploration.
11
11
  - When `docyrus/knowledge/` exists, keep it updated and finish local work with `docyrus knowledge check`.
12
+ - If the repository contains `docyrus/project-plan/project-plan.json`, use `docyrus project-plan show` to understand current work scope and `docyrus project-plan get-task` to inspect individual tasks before starting work. Update task status with `docyrus project-plan set-task-status` as work progresses.
12
13
  - Use the installed Docyrus skills as your command and workflow reference.
13
14
  - Be careful with tenant-scoped mutations. Confirm real identifiers and current context before changing state.
14
15
  - Treat auth files, tokens, and `.docyrus` contents as sensitive.
@@ -24,4 +25,15 @@ Docyrus concepts you should understand and use accurately:
24
25
  - discover commands and tenant OpenAPI inspection
25
26
  - raw API access via the CLI when needed
26
27
 
28
+ Project plan system:
29
+
30
+ The project-plan is a repo-tracked work graph stored at `docyrus/project-plan/project-plan.json`. It organizes work into sections (anchored to knowledge graph section IDs), features, and tasks. A derived `PROJECT_PLAN.md` is always kept in sync.
31
+
32
+ - `docyrus project-plan show` — view the full hierarchy with statuses
33
+ - `docyrus project-plan get-task --taskId <id>` — inspect a task and its linked local subtasks
34
+ - `docyrus project-plan set-task-status --taskId <id> --status <status>` — advance task status (`planned` → `in_progress` → `done`, or `blocked`)
35
+ - `docyrus project-plan check` — validate that all section references and task fields are consistent
36
+
37
+ When working on a repo that has a project plan, read it at the start of a session to understand scope and priorities. Update task status as work progresses and after it completes.
38
+
27
39
  You are not a generic shell assistant first. You are a Docyrus-first operator that happens to have local tools.
@@ -20,6 +20,7 @@ Core behavior:
20
20
  - Use the local `docyrus` CLI for Docyrus platform state, tenant context, schema inspection, app and data-source operations, API discovery, and data verification.
21
21
  - If the repository contains `docyrus/knowledge/`, use `docyrus knowledge search`, `docyrus knowledge section`, and `docyrus knowledge expand` before coding so you start from documented repo intent rather than only source inspection.
22
22
  - When `docyrus/knowledge/` exists, keep it in sync with behavior changes and finish with `docyrus knowledge check`.
23
+ - If the repository contains `docyrus/project-plan/project-plan.json`, read it at session start with `docyrus project-plan show` to understand current priorities and work scope. Update task status with `docyrus project-plan set-task-status` as work begins and finishes. Use `docyrus project-plan create-linked-todo` to break agent-assigned tasks into local subtasks when needed.
23
24
  - Prefer `--json` whenever command output needs to be parsed, compared, or fed back into reasoning.
24
25
  - Start from real state before making claims about apps, data sources, users, fields, enums, environments, auth state, API shape, or deployment context.
25
26
  - Distinguish clearly between local code changes and remote Docyrus platform mutations.
@@ -69,6 +70,23 @@ Schema-first workflow for new Docyrus-backed apps and major features:
69
70
  - After schema changes, verify the resulting metadata and runtime behavior with `docyrus ds get`, `docyrus ds list`, and `docyrus discover`.
70
71
  - When the project uses generated collections or codegen from the OpenAPI spec, resync or regenerate those artifacts after schema changes if the repo workflow requires it.
71
72
 
73
+ Project plan system:
74
+
75
+ The project-plan is a repo-tracked work graph stored at `docyrus/project-plan/project-plan.json` with a derived `PROJECT_PLAN.md` always kept in sync. Work is organized into sections (anchored to knowledge graph section IDs), features, and tasks. Tasks have a type (`new-implementation`, `bug-fix`, `api-test`, `browser-automation-test`, `work`), an assignee (`agent` or `user`), a status (`planned`, `in_progress`, `blocked`, `done`), and optional acceptance criteria.
76
+
77
+ Key commands:
78
+
79
+ - `docyrus project-plan show` — view the full hierarchy with feature and task statuses
80
+ - `docyrus project-plan get-task --taskId <id>` — inspect a specific task and its linked local subtasks
81
+ - `docyrus project-plan set-task-status --taskId <id> --status <status>` — advance task status
82
+ - `docyrus project-plan create-linked-todo --taskId <id> --title <title> --body <body>` — create a local `.pi/todos` subtask linked to an agent-assigned canonical task
83
+ - `docyrus project-plan upsert-feature --sectionId <id> --title <title> --slug <slug>` — create or update a feature
84
+ - `docyrus project-plan upsert-task --featureId <id> --title <title> --type <type> --assignee <assignee>` — create or update a task
85
+ - `docyrus project-plan check` — validate section references and graph integrity
86
+ - `docyrus project-plan ensure` — initialize the graph from knowledge sections if it does not yet exist
87
+
88
+ Workflow: read the project plan at session start → set relevant tasks to `in_progress` before beginning → create linked todos for complex implementation tasks → set tasks to `done` after work is verified.
89
+
72
90
  Docyrus CLI workflows you should rely on:
73
91
 
74
92
  - Use `docyrus auth who`, `docyrus auth accounts ...`, `docyrus auth tenants ...`, and `docyrus env ...` to confirm active identity, tenant, and environment.
@@ -24,7 +24,7 @@ Use this skill when you are:
24
24
  - Setting up authentication with `@docyrus/signin`
25
25
  - Bootstrapping tenant-aware runtime utilities with `@docyrus/app-utils`
26
26
  - Fetching or mutating data with generated collections or `@docyrus/api-client`
27
- - Persisting app-level config or saved grid views with `AppConfig` and `DataViews`
27
+ - Persisting app-level config or user-level config or saved grid views with `AppConfig`, `UserAppConfig`, and `DataViews`
28
28
  - Building record sharing, role management, or ACL-driven UI flows
29
29
  - Designing feature UIs such as dashboards, forms, tables, layouts, dialogs, analytics, or detail pages
30
30
  - Selecting between shadcn, diceui, animate-ui, docyrus-ui, and reui components
@@ -36,7 +36,7 @@ Use this skill when you are:
36
36
  2. Bootstrap `TenantPreferences`, date/number utilities, and shared app runtime helpers from `@docyrus/app-utils`.
37
37
  3. Use generated Docyrus collection hooks or the REST client for data access.
38
38
  4. Define `columns`, filters, formulas, child queries, and mutations correctly.
39
- 5. Use `AppConfig` for per-app persisted settings and `DataViews` for saved grid views.
39
+ 5. Use `AppConfig` for per-app persisted settings, `UserAppConfig` for per-user per-app settings, and `DataViews` for saved grid views.
40
40
  6. Check preferred UI components before building anything custom.
41
41
  7. Use Docyrus form and detail patterns for create, edit, item detail, and editable grid flows.
42
42
  8. Connect UI actions to TanStack Query mutations and invalidate relevant queries.
@@ -81,6 +81,7 @@ Use `@docyrus/app-utils` as the default runtime layer for tenant-level formattin
81
81
  ```tsx
82
82
  import {
83
83
  createAppConfigClient,
84
+ createUserAppConfigClient,
84
85
  createDataViewClient,
85
86
  createDateUtils,
86
87
  createNumberUtils,
@@ -109,6 +110,7 @@ function useAppRuntime(appId: string) {
109
110
  }),
110
111
  numberUtils: createNumberUtils({ preferences }),
111
112
  appConfig: createAppConfigClient(client!, appId),
113
+ userConfig: createUserAppConfigClient(client!, appId),
112
114
  dataViews: createDataViewClient(client!, appId),
113
115
  }
114
116
  },
@@ -121,6 +123,7 @@ Use this runtime to:
121
123
  - Format dates and datetimes with tenant format strings and the user's timezone.
122
124
  - Format numbers, currency-like values, and decimals using tenant separators and precision.
123
125
  - Read and upsert the app's single persisted `AppConfig` document.
126
+ - Read and upsert the current user's `UserAppConfig` document (per-user per-app settings).
124
127
  - Read and persist saved grid views through `DataViews`.
125
128
 
126
129
  ### Data fetching with generated collections
@@ -187,17 +190,18 @@ Use `DataGridViewSelect` as the default saved-view UI for Docyrus grids, and per
187
190
  8. **Initialize `TenantPreferences` once per app runtime** and create shared `dateUtils` / `numberUtils` instances from `@docyrus/app-utils`.
188
191
  9. **Formatting functions from `@docyrus/app-utils` are regionalized** — do not hardcode locale, date format, decimal separator, thousand separator, or decimal precision when tenant preferences should drive them.
189
192
  10. **Use `createAppConfigClient(client, appId)`** for the app's single persisted config document; `upsert` is the default write path.
190
- 11. **Use `createDataViewClient(client, appId)`** for saved grid-view CRUD.
191
- 12. **Use `DataViews` with `DataGridViewSelect`** to show, create, edit, reorder, hide, unhide, soft-delete, and hard-delete saved data grid views.
192
- 13. **`DataGridViewSelect` needs a TanStack table instance** and should receive `fields` when you want the built-in filter builder/editor experience.
193
- 14. **Data view creation requires `name` and `tenant_data_source_id`**.
194
- 15. **Use `dataViews.update(viewId, { archived: true })` for soft-delete** and `dataViews.remove(viewId)` only for irreversible hard-delete.
195
- 16. **Regenerate collections after schema changes** by rebuilding the tenant OpenAPI spec, downloading the latest `openapi.json`, and re-running the collection generator.
196
- 17. **ACL endpoints are usually raw-client integrations** use `useDocyrusClient()` or `RestApiClient` for roles, user-role assignments, role queries, record sharing, and ownership transfer.
197
- 18. **Prefer role `uid` values** for ACL role writes, user-role `roleIds`, and role-query `roleIds`.
198
- 19. **Treat `PUT /v1/users/acl/users/:userId/roles` as full replacement** and `POST /v1/users/acl/users/:userId/roles` as additive.
199
- 20. **Send role-query `query` as raw JSON** and omit `tenantAppId` when `dataSourceId` is present; backend derives it.
200
- 21. **After deleting a role, invalidate dependent app queries** for role lists, user-role lists, role-query lists, and any UI that renders primary-role labels.
193
+ 11. **Use `createUserAppConfigClient(client, appId)`** for the current user's persisted config document scoped to an app (e.g. theme, layout preferences, sidebar state); `upsert` is the default write path.
194
+ 12. **Use `createDataViewClient(client, appId)`** for saved grid-view CRUD.
195
+ 13. **Use `DataViews` with `DataGridViewSelect`** to show, create, edit, reorder, hide, unhide, soft-delete, and hard-delete saved data grid views.
196
+ 14. **`DataGridViewSelect` needs a TanStack table instance** and should receive `fields` when you want the built-in filter builder/editor experience.
197
+ 15. **Data view creation requires `name` and `tenant_data_source_id`**.
198
+ 16. **Use `dataViews.update(viewId, { archived: true })` for soft-delete** and `dataViews.remove(viewId)` only for irreversible hard-delete.
199
+ 17. **Regenerate collections after schema changes** by rebuilding the tenant OpenAPI spec, downloading the latest `openapi.json`, and re-running the collection generator.
200
+ 18. **ACL endpoints are usually raw-client integrations** — use `useDocyrusClient()` or `RestApiClient` for roles, user-role assignments, role queries, record sharing, and ownership transfer.
201
+ 19. **Prefer role `uid` values** for ACL role writes, user-role `roleIds`, and role-query `roleIds`.
202
+ 20. **Treat `PUT /v1/users/acl/users/:userId/roles` as full replacement** and `POST /v1/users/acl/users/:userId/roles` as additive.
203
+ 21. **Send role-query `query` as raw JSON** and omit `tenantAppId` when `dataSourceId` is present; backend derives it.
204
+ 22. **After deleting a role, invalidate dependent app queries** for role lists, user-role lists, role-query lists, and any UI that renders primary-role labels.
201
205
 
202
206
  ## Critical UI/UX Rules
203
207
 
package/server-loader.js CHANGED
@@ -25706,7 +25706,7 @@ async function createAgentServer(params) {
25706
25706
  app.post("/api/chat", async (c) => {
25707
25707
  const body2 = await c.req.json();
25708
25708
  const messages = body2.messages ?? [];
25709
- if (body2.sessionId) {
25709
+ if (body2.sessionId && body2.sessionId.trim() !== activeSession.id?.trim()) {
25710
25710
  try {
25711
25711
  activeSession = await onResumeSession(body2.sessionId);
25712
25712
  } catch (error) {
@@ -25829,6 +25829,21 @@ async function createAgentServer(params) {
25829
25829
  messageCount: s.messageCount,
25830
25830
  firstMessage: s.firstMessage
25831
25831
  })).sort((a, b) => b.modified.localeCompare(a.modified));
25832
+ const activeId = activeSession.id?.trim();
25833
+ if (activeId && !mapped.some((s) => s.id === activeId)) {
25834
+ const now = (/* @__PURE__ */ new Date()).toISOString();
25835
+ mapped.unshift({
25836
+ id: activeId,
25837
+ path: "",
25838
+ cwd: context.cwd,
25839
+ name: null,
25840
+ parentSessionPath: null,
25841
+ created: now,
25842
+ modified: now,
25843
+ messageCount: 0,
25844
+ firstMessage: null
25845
+ });
25846
+ }
25832
25847
  return c.json({ sessions: mapped });
25833
25848
  } catch (error) {
25834
25849
  const message = error instanceof Error ? error.message : String(error);
@@ -26434,11 +26449,13 @@ async function createAgentServer(params) {
26434
26449
  app.post("/api/sessions/:sessionId/resume", async (c) => {
26435
26450
  const sessionId = c.req.param("sessionId");
26436
26451
  try {
26437
- if (activeSession.isStreaming) {
26438
- await activeSession.abort();
26439
- await waitForIdle(activeSession);
26452
+ if (sessionId.trim() !== activeSession.id?.trim()) {
26453
+ if (activeSession.isStreaming) {
26454
+ await activeSession.abort();
26455
+ await waitForIdle(activeSession);
26456
+ }
26457
+ activeSession = await onResumeSession(sessionId);
26440
26458
  }
26441
- activeSession = await onResumeSession(sessionId);
26442
26459
  return c.json({
26443
26460
  ok: true,
26444
26461
  sessionId,
@@ -26953,6 +26970,29 @@ async function createAgentServer(params) {
26953
26970
  return c.json({ error: message }, 500);
26954
26971
  }
26955
26972
  });
26973
+ app.get("/api/git/commits", async (c) => {
26974
+ const cwd = context.cwd;
26975
+ const limit = Math.min(Math.max(Number(c.req.query("limit")) || 50, 1), 500);
26976
+ try {
26977
+ let raw2 = "";
26978
+ try {
26979
+ raw2 = await gitExec(
26980
+ ["log", `--max-count=${limit}`, "--format=%H%x00%an%x00%ae%x00%aI%x00%s"],
26981
+ cwd
26982
+ );
26983
+ } catch {
26984
+ return c.json({ commits: [] });
26985
+ }
26986
+ const commits = raw2.trim().split("\n").filter(Boolean).map((line) => {
26987
+ const [hash, authorName, authorEmail, date, message] = line.split("\0");
26988
+ return { hash, authorName, authorEmail, date, message };
26989
+ });
26990
+ return c.json({ commits });
26991
+ } catch (error) {
26992
+ const message = error instanceof Error ? error.message : String(error);
26993
+ return c.json({ error: message }, 500);
26994
+ }
26995
+ });
26956
26996
  app.get("/api/mcp/servers", async (c) => {
26957
26997
  try {
26958
26998
  const [config, cache, provenance] = await Promise.all([