@neuralis/brain-core 0.2.0 → 0.3.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.
Files changed (123) hide show
  1. package/README.md +8 -3
  2. package/app/README.md +1 -1
  3. package/app/filesystem/README.md +3 -3
  4. package/app/filesystem/api/README.md +4 -4
  5. package/app/filesystem/api/index.ts +144 -139
  6. package/app/filesystem/domain/types.ts +12 -0
  7. package/app/filesystem/hooks/README.md +5 -5
  8. package/app/filesystem/hooks/useDiscoverableSources.ts +4 -2
  9. package/app/filesystem/hooks/useFilesystemSSE.ts +31 -23
  10. package/app/filesystem/hooks/useGitChangesInventory.ts +4 -2
  11. package/app/filesystem/host/README.md +3 -2
  12. package/app/filesystem/host/installFilesystemHostComponents.tsx +22 -9
  13. package/app/filesystem/store/README.md +12 -8
  14. package/app/filesystem/store/filesystemStore.ts +444 -163
  15. package/app/filesystem/store/gitSlice.ts +6 -6
  16. package/app/filesystem/store/listSlice.ts +9 -6
  17. package/app/filesystem/store/searchSlice.ts +3 -3
  18. package/app/filesystem/ui/FileEditor.tsx +90 -39
  19. package/app/filesystem/ui/FileTree.tsx +10 -9
  20. package/app/filesystem/ui/GitCommitChangesView.tsx +5 -3
  21. package/app/filesystem/ui/GitCommitGraph.tsx +6 -5
  22. package/app/filesystem/ui/GitControlPanel.tsx +14 -14
  23. package/app/filesystem/ui/GitDiffView.tsx +14 -6
  24. package/app/filesystem/ui/GitFileDiff.tsx +22 -15
  25. package/app/filesystem/ui/PendingChangesPanel.tsx +108 -44
  26. package/app/filesystem/ui/PendingReviewDialog.tsx +8 -5
  27. package/app/filesystem/ui/README.md +9 -9
  28. package/app/filesystem/ui/ReembedDialog.tsx +4 -2
  29. package/app/filesystem/ui/SourcesPanel.tsx +26 -28
  30. package/app/filesystem/ui/Toolbar.tsx +5 -4
  31. package/app/filesystem/ui/VectorHealthPanel.tsx +48 -17
  32. package/dist/app/host.js +46 -46
  33. package/dist/src/config/env.d.ts +4 -0
  34. package/dist/src/config/env.d.ts.map +1 -1
  35. package/dist/src/config/env.js +13 -0
  36. package/dist/src/config/env.js.map +1 -1
  37. package/dist/src/connectors/LocalDiskConnector.d.ts +9 -1
  38. package/dist/src/connectors/LocalDiskConnector.d.ts.map +1 -1
  39. package/dist/src/connectors/LocalDiskConnector.js +51 -2
  40. package/dist/src/connectors/LocalDiskConnector.js.map +1 -1
  41. package/dist/src/domain/healthTypes.d.ts +1 -7
  42. package/dist/src/domain/healthTypes.d.ts.map +1 -1
  43. package/dist/src/domain/pendingChange.d.ts +31 -0
  44. package/dist/src/domain/pendingChange.d.ts.map +1 -1
  45. package/dist/src/domain/pendingChange.js.map +1 -1
  46. package/dist/src/routes/health.js +2 -3
  47. package/dist/src/routes/health.js.map +1 -1
  48. package/dist/src/routes/pending.d.ts +7 -7
  49. package/dist/src/routes/pending.d.ts.map +1 -1
  50. package/dist/src/routes/pending.js +21 -21
  51. package/dist/src/routes/pending.js.map +1 -1
  52. package/dist/src/services/FilesystemService.d.ts +13 -61
  53. package/dist/src/services/FilesystemService.d.ts.map +1 -1
  54. package/dist/src/services/FilesystemService.js +47 -189
  55. package/dist/src/services/FilesystemService.js.map +1 -1
  56. package/dist/src/services/FilesystemUiService.d.ts +33 -1
  57. package/dist/src/services/FilesystemUiService.d.ts.map +1 -1
  58. package/dist/src/services/FilesystemUiService.js +50 -1
  59. package/dist/src/services/FilesystemUiService.js.map +1 -1
  60. package/dist/src/store/PendingChangeStore.d.ts +3 -0
  61. package/dist/src/store/PendingChangeStore.d.ts.map +1 -1
  62. package/dist/src/store/PendingChangeStore.js +84 -25
  63. package/dist/src/store/PendingChangeStore.js.map +1 -1
  64. package/dist/src/usecases/orphanedPending.d.ts +2 -2
  65. package/dist/src/usecases/orphanedPending.d.ts.map +1 -1
  66. package/dist/src/usecases/orphanedPending.js +5 -3
  67. package/dist/src/usecases/orphanedPending.js.map +1 -1
  68. package/dist/src/usecases/pendingListing.d.ts +34 -0
  69. package/dist/src/usecases/pendingListing.d.ts.map +1 -0
  70. package/dist/src/usecases/pendingListing.js +244 -0
  71. package/dist/src/usecases/pendingListing.js.map +1 -0
  72. package/dist/src/usecases/vectorMaintenance.d.ts +6 -9
  73. package/dist/src/usecases/vectorMaintenance.d.ts.map +1 -1
  74. package/dist/src/usecases/vectorMaintenance.js +4 -13
  75. package/dist/src/usecases/vectorMaintenance.js.map +1 -1
  76. package/dist/src/vector/InMemoryVectorIndex.d.ts.map +1 -1
  77. package/dist/src/vector/InMemoryVectorIndex.js +30 -9
  78. package/dist/src/vector/InMemoryVectorIndex.js.map +1 -1
  79. package/package.json +27 -5
  80. package/rules/brain-rules.md +1 -1
  81. package/skills/create-package/SKILL.md +3 -3
  82. package/skills/create-package/references/config-and-credentials.md +1 -1
  83. package/skills/create-package/references/manifest-and-discovery.md +1 -1
  84. package/skills/create-package/references/runtime-and-build.md +1 -1
  85. package/skills/create-package/references/skills-and-context.md +25 -12
  86. package/skills/create-package/references/surfaces-and-design.md +7 -5
  87. package/skills/create-package/references/test-and-ship.md +9 -16
  88. package/skills/create-package/scripts/check-package.sh +47 -1
  89. package/skills/create-package/scripts/verify-loaded.sh +18 -27
  90. package/skills/git/SKILL.md +27 -29
  91. package/skills/manage-files/SKILL.md +37 -32
  92. package/skills/manage-files/scripts/upload.sh +7 -9
  93. package/skills/manage-files/scripts/write.sh +2 -6
  94. package/skills/manage-sources/SKILL.md +93 -95
  95. package/skills/manage-sources/scripts/reembed.sh +2 -6
  96. package/skills/manage-uri-policy/SKILL.md +30 -58
  97. package/app/filesystem/hooks/useContainerWidth.ts +0 -39
  98. package/skills/git/scripts/git.sh +0 -48
  99. package/skills/manage-files/scripts/approve.sh +0 -9
  100. package/skills/manage-files/scripts/list.sh +0 -9
  101. package/skills/manage-files/scripts/pending.sh +0 -23
  102. package/skills/manage-files/scripts/raw.sh +0 -11
  103. package/skills/manage-files/scripts/read.sh +0 -9
  104. package/skills/manage-files/scripts/search.sh +0 -15
  105. package/skills/manage-sources/scripts/browse.sh +0 -12
  106. package/skills/manage-sources/scripts/cancel.sh +0 -14
  107. package/skills/manage-sources/scripts/create.sh +0 -13
  108. package/skills/manage-sources/scripts/delete.sh +0 -17
  109. package/skills/manage-sources/scripts/discoverable.sh +0 -6
  110. package/skills/manage-sources/scripts/get.sh +0 -7
  111. package/skills/manage-sources/scripts/kind-schema.sh +0 -7
  112. package/skills/manage-sources/scripts/kinds.sh +0 -6
  113. package/skills/manage-sources/scripts/list.sh +0 -6
  114. package/skills/manage-sources/scripts/purge-index.sh +0 -18
  115. package/skills/manage-sources/scripts/reindex.sh +0 -13
  116. package/skills/manage-sources/scripts/reset.sh +0 -11
  117. package/skills/manage-sources/scripts/set-enabled.sh +0 -17
  118. package/skills/manage-sources/scripts/sync-status.sh +0 -14
  119. package/skills/manage-sources/scripts/sync.sh +0 -13
  120. package/skills/manage-sources/scripts/update.sh +0 -18
  121. package/skills/manage-uri-policy/scripts/manifests.sh +0 -20
  122. package/skills/manage-uri-policy/scripts/set-uri.sh +0 -66
  123. package/skills/manage-uri-policy/scripts/show-uri.sh +0 -23
@@ -15,7 +15,7 @@ Every surface lives in `package.json` under `neuralis.app.surfaces[]` — a unio
15
15
  | kind | required | useful optional |
16
16
  |---|---|---|
17
17
  | `widget` | `kind`, `id`, `type`, `title`, `component` | `icon`, `layout`, `chrome`, `open`, `defaultOpen`, `requires` |
18
- | `dock` | `kind`, `id`, `label`, `action` | `icon`, `position`, `section`, `dock`, `tooltip`, `color` |
18
+ | `dock` | `kind`, `id`, `label`, `action` | `icon`, `image`, `position`, `section`, `dock`, `tooltip`, `color` |
19
19
  | `card` | `kind`, `id`, `type` | `title`, `render`, `match`, `bridge` |
20
20
 
21
21
  **Every icon comes from ONE platform library** — about 270 curated lucide names in categories, the `ICON_LIBRARY` export of `@neuralis/package-system/icons`. Write the PascalCase name (`ChartLine`) or its lucide-kebab form (`chart-line`); a few aliases (`layout`, `terminal`, `admin`) also resolve. The same rule covers a surface `icon`, a team member's `appearance.icon` and a workflow template's `icon`. Any other name renders the fallback glyph, warns at load, and is an ERROR in `check-package.sh`.
@@ -68,6 +68,8 @@ The smallest complete, **reachable** UI a project-dropped package can ship. Four
68
68
 
69
69
  **Gate the widget, not the dock entry** — dock visibility inherits the target widget's `requires.features` automatically. Duplicating `requires` only creates drift: a user holding the widget's features but not your accidentally-different dock feature gets a widget they can never open.
70
70
 
71
+ **An app picture is optional: `"image": "app/shared/<name>.png"`.** A PNG, JPEG or WebP under `app/shared/`, at most 256 KiB, each path segment letters, digits, `.`, `_` or `-`. The dock, the app menu and the inbox show it in a fixed-size box to whoever can see the dock entry; keep the `icon` glyph — it renders until the picture arrives and whenever it cannot be shown. The platform judges the BYTES, never the extension: an SVG, a renamed document or an oversized file is not served and the glyph stays. An invalid path (absolute, remote, outside `app/shared/`, `..`, a non-raster extension) is a validation error that fails the whole package. `check-package.sh` checks the path, the file and its bytes.
72
+
71
73
  ## Task: pick a renderer
72
74
 
73
75
  **Two renderers exist, and both render.** There is no third option and no placeholder tier: `inline-html` and `wasm-ui` were removed from the contract, and a manifest still declaring one is rejected outright — *renderer 'inline-html' was removed; ship the HTML asset and declare renderer 'iframe' with a relative url*. A leftover `fallback` key is inert and warned about. (WASM remains the **tool/route** sandbox runtime; there has never been a WASM UI engine, and one can no longer be declared.)
@@ -117,7 +119,7 @@ Five things to know before you use it:
117
119
  - **Budgets: 64 distinct paths and 8 MiB per SCOPE**, and the lane is `no-store`, so every render refetches the whole set. The preflight errors past both numbers and warns past 1 MiB — but it counts only your SURFACE tree, while the runtime budget also charges every `app/shared/` file your entry pulls in, so a shared-heavy bundle can pass authoring and still be refused at request time. The per-file 2 MiB cap applies in both modes.
118
120
  - **Still no remote origins.** The CSP names none, so a CDN script or a Google font is blocked before the request leaves. Ship the file.
119
121
 
120
- `app/shared/` is a declared **package-public** namespace: any visible surface of the package may read it, so nothing feature-private belongs there.
122
+ `app/shared/` is a declared **package-public** namespace: any visible surface of the package may read it, and a declared dock picture is served from it, so nothing feature-private belongs there.
121
123
 
122
124
  ## The silent failures — symptom first
123
125
 
@@ -167,7 +169,7 @@ Asset serving is authenticated per request, scope-bound, traversal-safe, `Cache-
167
169
 
168
170
  *Symptom of omitting `color-scheme` or an explicit height:* the frame collapses to the HTML default 150px, and the browser paints an opaque **white** backdrop behind a cross-origin frame whose colour scheme differs from the dark workspace. Declare `color-scheme: dark light` at **every** nesting level — outer element and embedded document both.
169
171
 
170
- Host CSS custom properties (`--color-text`, `--w-bg-rgb`, `--icon-accent`, `--control-surface`, …) reach `direct`-rendered components only; there, never hardcode a hex — three themes ship — and `--w-bg-rgb` is comma-separated channels: `rgba(var(--w-bg-rgb), 0.55)`.
172
+ Host CSS custom properties (`--color-text`, `--w-bg-rgb`, `--icon-accent`, `--control-surface`, …) reach `direct`-rendered components only; there, never hardcode a hex — four themes ship, independently selected for widgets, docks and background — and `--w-bg-rgb` is comma-separated channels: `rgba(var(--w-bg-rgb), 0.55)`. Every theme is dark polarity (white text), so design once for the dark design.
171
173
 
172
174
  ### Design floor
173
175
 
@@ -183,7 +185,7 @@ Host CSS custom properties (`--color-text`, `--w-bg-rgb`, `--icon-accent`, `--co
183
185
 
184
186
  `chrome.mode` is `toolbar` (default), `frameless` or `floating`. All three render the **same** host control strip, differing only in placement and visibility. `toolbar`/`floating` wrap content with padding and automatic scroll; **`frameless` clips content with no padding and no scrollbar** — the widget root owns its scroll and padding and must reserve roughly 4rem at the top right. *Symptom of ignoring it:* your top-right controls sit under the hover-revealed host strip and become unclickable, and content is cut off with no way to scroll. Iframe widgets usually want `frameless`.
185
187
 
186
- **The close (X) button minimises; it does not destroy.** The widget stays mounted off-screen and survives a refresh. *Symptom:* reopening shows it exactly as it was, "fresh start" initialisation never runs, and teardown in an unmount handler never fires. True destroy is the dock item's hover trash. **Exception for `renderer: "iframe"` widgets:** only the React subtree stays mounted — the frame itself is detached from the document while minimised (and on a stage-mode switch), so the browser discards its browsing context and the frame reloads from the same URL with everything inside it gone. *Symptom:* your in-frame bootstrap DOES re-run and any in-frame in-memory state is lost. Make an iframe widget's bootstrap re-runnable and persist state server-side or in the frame's own storage.
188
+ **The close (X) button minimises; it does not destroy.** The widget stays mounted off-screen and survives a refresh. *Symptom:* reopening shows it exactly as it was, "fresh start" initialisation never runs, and teardown in an unmount handler never fires. True destroy is the dock item's clean (broom) control. **Exception for `renderer: "iframe"` widgets:** only the React subtree stays mounted — the frame itself is detached from the document while minimised (and on a stage-mode switch), so the browser discards its browsing context and the frame reloads from the same URL with everything inside it gone. *Symptom:* your in-frame bootstrap DOES re-run and any in-frame in-memory state is lost. Make an iframe widget's bootstrap re-runnable and persist state server-side or in the frame's own storage.
187
189
 
188
190
  Transparency resolves per-instance override → manifest `chrome.transparent` → the user's Layout default → opaque; the cycle control is hidden when `chrome.userTransparency: false` or the package is untrusted. **The level is a user setting your surface cannot read, and it changes the panel's CSS containment**: the panel emits a `backdrop-filter` at `dim` and `opaque` (not at `transparent`), so at those two levels it is a containing block for `position: fixed` descendants. Design so the level never matters — portal every `fixed` overlay to `document.body` (see the Design floor above). The panel's stacking context does not vary: it is isolated at every level.
189
191
 
@@ -274,7 +276,7 @@ registerCardComponent('thing.result', ThingCard, {
274
276
 
275
277
  `component.import` resolves through a client-side host registry the package populates from its runtime UI module: declare `app.module`, build `app/host.tsx` with `neuralis-build ui`, and the host imports the module and calls each `install…HostComponents({ registry, port })` export with a registry that accepts only your own package's registrations. Every `component.import` your manifest declares must be registered somewhere under `app/`, or the surface resolves to null and shows the placeholder — no error. A module built before a platform upgrade can be refused (its record names an older host API or no React major): rebuild it with `neuralis-build ui`.
276
278
 
277
- The component receives exactly `{ agentId, widgetId, state }`; everything else comes from the workspace host port captured at install time — project/user ids, agents, `openWidget`, `updateWidgetState`, `getGrantedFeatures`, `useVisibleSkills`, and `subscribeRealtime({ channel, projectId, agentId?, onEvent })` over `runtime | presence | workflow | filesystem | conversation | project` (plus the optional `isRealtimeConnected()` for that connection's current state — treat its absence as "may be disconnected"). If your package keeps its own client store keyed on the active project/agent, subscribe to the optional `onClientStateReset(handler)` at install time: the dock's clean (broom) control fires it at widget level (with the widget `type` — reset only on YOUR type), agent level and project level, and your handler drops that scope's client state (persisted tabs included) without calling the host back or touching the server. A host without the method never resets you; assume nothing. HTTP goes through the generic package API client, which injects the project and agent headers.
279
+ The component receives exactly `{ agentId, widgetId, state }` — `agentId` is the window's BOUND agent (fixed for that window, never the workspace's active one) and `widgetId` names the window; everything else comes from the workspace host port captured at install time — project/user ids, agents, `openWidget`, `updateWidgetState`, `getGrantedFeatures`, `useVisibleSkills`, and `subscribeRealtime({ channel, projectId, agentId?, onEvent })` over `runtime | presence | workflow | filesystem | conversation | project` (plus the optional `isRealtimeConnected()` for that connection's current state — treat its absence as "may be disconnected"). A widget can be open in SEVERAL windows (unless its manifest sets `open.singleton: true`): keep one client store PER WINDOW, keyed on `widgetId` and the bound `agentId`, never one global store read through the active agent — two windows must never share a selection, a draft or a late response. `openWidget({ agentId, type, title, target? })` picks the window: absent or `'default'` is the most recently focused window of that type bound to that agent (opened only when none exists), `'new'` always opens another (a singleton answers as `'default'`), and `{ instanceId }` is exactly that window. Subscribe to the optional `onClientStateReset(handler)` at install time: the dock's clean (broom) control fires it at four levels — `instance` (clean ONE window, `instanceId`; the window stays open), `widget` (that window is closed; reset only on YOUR `widgetType`), `agent` and `project` — and your handler drops that scope's client state (persisted tabs included) without calling the host back or touching the server. A host without the method never resets you; assume nothing. HTTP goes through the generic package API client, which injects the project and agent headers — build it from the window's bound agent.
278
280
 
279
281
  ### Offer the skill next to the button that does the same thing
280
282
 
@@ -77,16 +77,14 @@ grouping reason and the per-category file counts in place of a status.
77
77
  Where to read it without server-log access:
78
78
 
79
79
  ```bash
80
- curl -sS -w '\nHTTP %{http_code}\n' \
81
- -H "Authorization: Bearer ${NEURALIS_SESSION_TOKEN}" \
82
- "${NEURALIS_API}/api/packages/@neuralis/agent-core/packages/diagnostics"
80
+ neuralis request GET @neuralis/agent-core/packages/diagnostics
83
81
  ```
84
82
 
85
- Both variables are already in a skill's environment. There is **no project header**: the catch-all
86
- derives the project from the verified ticket, and a project id you supply that disagrees with the
87
- ticket's is refused outright. Without that bearer (or a browser cookie session) the call is a flat
88
- `401` before anything else is read — caller-supplied identity is not an auth mechanism anywhere on
89
- this platform.
83
+ Inside a skill's shell it authenticates with the session ticket already in the environment.
84
+ There is **no project header**: the catch-all derives the project from the verified ticket, and a
85
+ project id you supply that disagrees with the ticket's is refused outright. Without that ticket (or
86
+ a `neuralis login` grant, or a browser cookie session) the call is a flat `401` before anything else
87
+ is read — caller-supplied identity is not an auth mechanism anywhere on this platform.
90
88
 
91
89
  Entries carry `{ packageId, status, reason?, error?, trust, registered }`. The route is gated on
92
90
  the `core.observe` feature: **a `403` here means you lack the diagnostics capability, not that the
@@ -364,19 +362,14 @@ invisible, with **no error anywhere** — indistinguishable from correctly hidde
364
362
 
365
363
  - `data://` and `packages://` ship with `trigger: "auto"`, so a disk-level write is picked up by the
366
364
  file watcher after roughly a two-second debounce. **Nothing to call.**
367
- - On any other source, or one set to `manual`: **no tool syncs.** Call the route from the same shell —
368
- both variables are already in a skill's environment:
365
+ - On any other source, or one set to `manual`: **no tool syncs.** Call the route from the same shell:
369
366
 
370
367
  ```bash
371
- curl --fail-with-body -sS -X POST \
372
- -H "Authorization: Bearer ${NEURALIS_SESSION_TOKEN}" \
373
- -H "Content-Type: application/json" \
374
- -d '{"source":"data"}' \
375
- "${NEURALIS_API}/api/packages/@neuralis/brain-core/sources/sync"
368
+ neuralis request POST @neuralis/brain-core/sources/sync -f source=data
376
369
  ```
377
370
 
378
371
  It answers `202` with a job snapshot; the run is asynchronous, so poll
379
- `GET …/sources/sync-status?source=data`.
372
+ `neuralis request GET @neuralis/brain-core/sources/sync-status -f source=data`.
380
373
  - **Without `drive.sync` that call returns `403 {"error":"Sync requires drive.sync."}`.** Do not retry
381
374
  it and do not route around it — say which thing is off, in these words: *"I wrote the package files
382
375
  to `<uri>`. Indexing them needs a sync, which requires the `drive.sync` feature my role does not
@@ -568,10 +568,56 @@ if (man) {
568
568
  for (const [f, t] of toolCardTypes) {
569
569
  if (!byType.has(t)) warn(f, `x-neuralis.ui.cardType "${t}" matches no card surface declared here — the result falls back to the generic default card. Deliberate when the type is a host/builtin card; otherwise add {"kind":"card","id":"…","type":"${t}"}`);
570
570
  }
571
+ const imageDocks = docks.filter((d) => d.image !== undefined);
571
572
  if (hasShared) {
572
573
  warn(`${SHARED_DIR}/`, anyBundle
573
574
  ? `declared package-public: any visible surface of this package may read it, so no feature-private asset belongs there. It IS served — a bundle surface reaches it as '../shared/…' relative to the entry's own directory, so mind the depth (an entry at 'sub/index.html' needs '../../shared/…')`
574
- : `declared package-public: any visible surface of this package may read it, so no feature-private asset belongs there. No surface here declares "assetMode":"bundle", so nothing serves it — a self-contained entry requests it with no session cookie. Declare bundle mode on the surface that needs it, or inline the asset`);
575
+ : `declared package-public: any visible surface of this package may read it, so no feature-private asset belongs there. No surface here declares "assetMode":"bundle", so ${imageDocks.length ? 'only the declared dock picture(s) are served from it' : 'nothing serves it'} — a self-contained entry requests it with no session cookie. Declare bundle mode on the surface that needs it, or inline the asset`);
576
+ }
577
+
578
+ // dock pictures — an optional raster `image` on a dock entry. The PATH rule is
579
+ // the platform's own (`resolveDockImagePath`), the BYTE rule its own raster
580
+ // sniff and 256 KiB floor (`@neuralis/package-system/web-common`); where those
581
+ // do not resolve from here, the check is SKIPPED and says so.
582
+ if (imageDocks.length) {
583
+ let kernel = null, raster = null;
584
+ for (const from of [ROOT, process.cwd(), SCRIPT_DIR]) {
585
+ try {
586
+ kernel = require(require.resolve('@neuralis/package-system', { paths: [from] }));
587
+ raster = require(require.resolve('@neuralis/package-system/web-common', { paths: [from] }));
588
+ break;
589
+ } catch { kernel = null; raster = null; }
590
+ }
591
+ if (!kernel || typeof kernel.resolveDockImagePath !== 'function' || !raster || typeof raster.sniffRasterImage !== 'function') {
592
+ F.push({ lvl: 'SKIP', f: MF, m: `${imageDocks.length} dock image(s) NOT checked — @neuralis/package-system does not resolve from here; run the check where the platform is installed` });
593
+ } else {
594
+ let sharedReal = null;
595
+ try { sharedReal = fs.realpathSync(at('app', 'shared')); } catch { /* no app/shared */ }
596
+ for (const d of imageDocks) {
597
+ const L = `dock "${d.id || '(no id)'}" image`;
598
+ const resolved = typeof d.image === 'string' ? kernel.resolveDockImagePath(d.image) : { ok: false, error: 'not a string' };
599
+ if (!resolved.ok) {
600
+ err(MF, `${L} is not a valid picture path (${resolved.error}) — declare a relative path under '${SHARED_DIR}/' naming a .png, .jpg, .jpeg or .webp file (letters, digits, '.', '_', '-' per segment). The package does NOT load with this value`);
601
+ continue;
602
+ }
603
+ let real = null;
604
+ try { real = fs.realpathSync(at(...resolved.path.split('/'))); } catch { /* missing */ }
605
+ if (!real) { err(resolved.path, `${L} is declared but not on disk — the dock shows only its glyph`); continue; }
606
+ if (!sharedReal || !real.startsWith(sharedReal + path.sep)) {
607
+ err(resolved.path, `${L} resolves outside '${SHARED_DIR}/' through a link — the platform refuses to serve it`);
608
+ continue;
609
+ }
610
+ const st = fs.statSync(real);
611
+ if (!st.isFile()) { err(resolved.path, `${L} is not a regular file`); continue; }
612
+ if (st.size > raster.APPEARANCE_IMAGE_MAX_BYTES) {
613
+ err(resolved.path, `${L} is ${st.size} bytes; a package picture is served only up to ${raster.APPEARANCE_IMAGE_MAX_BYTES} bytes (256 KiB) — downscale it, a dock tile is a few dozen pixels`);
614
+ continue;
615
+ }
616
+ if (!raster.sniffRasterImage(new Uint8Array(fs.readFileSync(real)))) {
617
+ err(resolved.path, `${L} is not a PNG, JPEG or WebP picture by its BYTES — the platform sniffs the content, never the extension, and serves nothing else (an SVG or a renamed document is refused)`);
618
+ }
619
+ }
620
+ }
575
621
  }
576
622
 
577
623
  // icons — a name outside the platform icon library renders the fallback
@@ -9,8 +9,9 @@
9
9
  # only what the platform said.
10
10
  #
11
11
  # WHAT IT CHECKS
12
- # Reads the agent-core package diagnostics and overview surfaces as YOU, with
13
- # the session ticket already in the script environment, and reports one of:
12
+ # Reads the agent-core package diagnostics and overview surfaces as YOU through
13
+ # `neuralis request` (the session ticket inside an activated skill, your
14
+ # `neuralis login` on your own machine), and reports one of:
14
15
  # loaded — registered and operational
15
16
  # partial — declarative contributions registered, no code runtime; prints
16
17
  # the reason (build-missing | runtime-unavailable |
@@ -52,7 +53,7 @@
52
53
  # 1 = partial (reason printed)
53
54
  # 2 = absent — not listed for this caller
54
55
  # 3 = error / missing (recorded message printed)
55
- # 4 = could not reach the diagnostics surface (no session ticket, no
56
+ # 4 = could not reach the diagnostics surface (no `neuralis` CLI or login, no
56
57
  # network, HTTP failure, or the caller lacks the `core.observe`
57
58
  # feature). NOTHING is claimed about the package in this state.
58
59
  #
@@ -127,8 +128,8 @@ if [ -z "$PKG_ID" ]; then
127
128
  echo "usage: bash verify-loaded.sh <package-id> [--raw]" >&2
128
129
  exit 4
129
130
  fi
130
- if ! command -v curl >/dev/null 2>&1; then
131
- echo "ERROR curl not found on PATH" >&2
131
+ if ! command -v neuralis >/dev/null 2>&1; then
132
+ echo "ERROR neuralis (the CLI) not found on PATH" >&2
132
133
  manual_fallback
133
134
  exit 4
134
135
  fi
@@ -137,30 +138,20 @@ if ! command -v node >/dev/null 2>&1; then
137
138
  manual_fallback
138
139
  exit 4
139
140
  fi
140
- if [ -z "${NEURALIS_API:-}" ]; then
141
- echo "ERROR NEURALIS_API is not set — activate this skill first (execute action=\"skill\"); the API base and the session ticket are injected at activation" >&2
142
- manual_fallback
143
- exit 4
144
- fi
145
- if [ -z "${NEURALIS_SESSION_TOKEN:-}" ]; then
146
- echo "ERROR NEURALIS_SESSION_TOKEN is not set — activate this skill first; every internal call authenticates with the per-stream session ticket" >&2
147
- manual_fallback
148
- exit 4
149
- fi
150
-
151
- API_BASE="${NEURALIS_API%/}"
152
- BASE_ROUTE="${API_BASE}/api/packages/@neuralis/agent-core/packages"
141
+ BASE_ROUTE="@neuralis/agent-core/packages"
153
142
 
154
143
  WORK="$(mktemp -d)"
155
144
  trap 'rm -rf "$WORK"' EXIT
156
145
 
157
- fetch() { # fetch <url> <outfile> -> echoes the HTTP status ("000" = no response)
158
- local code
159
- code="$(curl -sS -o "$2" -w '%{http_code}' \
160
- -H "Authorization: Bearer ${NEURALIS_SESSION_TOKEN}" \
161
- -H "X-Neuralis-Skill: create-package" \
162
- "$1" 2>"$WORK/curl.err")" || true
163
- [ -n "$code" ] || code="000"
146
+ # `neuralis request` prints a 2xx body on stdout (exit 0); a non-2xx body goes to
147
+ # stderr with an `HTTP <code>` line (exit 1). No such line = no answer
148
+ # at all (no CLI login, no network) — "000". Its error text stays in request.err.
149
+ fetch() { # fetch <route> <outfile> -> echoes the HTTP status ("000" = no response)
150
+ local rc=0 code
151
+ neuralis request GET "$1" >"$2" 2>"$WORK/request.err" || rc=$?
152
+ if [ "$rc" -eq 0 ]; then echo 200; return; fi
153
+ code="$(sed -n '/^HTTP [0-9][0-9][0-9]$/{s/^HTTP //p;q;}' "$WORK/request.err")"
154
+ if [ -n "$code" ]; then awk '!d && /^HTTP [0-9][0-9][0-9]$/ {d=1; next} {print}' "$WORK/request.err" > "$2"; else code="000"; fi
164
155
  echo "$code"
165
156
  }
166
157
 
@@ -174,7 +165,7 @@ if [ "$DIAG_CODE" = "403" ]; then
174
165
  fi
175
166
  if [ "$DIAG_CODE" != "200" ]; then
176
167
  echo "ERROR GET ${BASE_ROUTE}/diagnostics returned HTTP ${DIAG_CODE}" >&2
177
- [ -s "$WORK/curl.err" ] && sed 's/^/ /' "$WORK/curl.err" >&2
168
+ [ -s "$WORK/request.err" ] && sed 's/^/ /' "$WORK/request.err" >&2
178
169
  [ -s "$WORK/diag.json" ] && head -c 600 "$WORK/diag.json" >&2 && echo >&2
179
170
  manual_fallback
180
171
  exit 4
@@ -322,7 +313,7 @@ const counts = () => {
322
313
  process.stdout.write(' drop — and into NO tenant at all when the package is neither\n');
323
314
  process.stdout.write(' first-party nor owned by a resolvable project, which is silent\n');
324
315
  process.stdout.write(' apart from one server log line. Prove the instance exists with\n');
325
- process.stdout.write(' GET <api>/api/packages/@neuralis/agent-core/connectors; that list is\n');
316
+ process.stdout.write(' neuralis request GET @neuralis/agent-core/connectors; that list is\n');
326
317
  process.stdout.write(' the platform lane plus your own project, and nothing else.\n');
327
318
  }
328
319
  if (overEntry.enabled === false) {
@@ -14,28 +14,32 @@ panel drives**, so an agent gets exactly the rights a human member has: gated by
14
14
  before every destructive op** and **credential-safe remotes** (the push/pull token is
15
15
  resolved server-side and never touches the agent shell).
16
16
 
17
- Call them with the one generic helper — `bash ${SKILL_DIR}/scripts/git.sh <GET|POST> <route> …`
18
- — which authenticates with the per-stream session ticket (`NEURALIS_API` +
19
- `NEURALIS_SESSION_TOKEN`, both injected at activation). This is *not* about committing to
20
- the Neuralis repo itself — it is about an agent operating git on a user's source.
17
+ Call them with `neuralis request <GET|POST> @neuralis/brain-core/git/<route> …` — inside an
18
+ activated skill it authenticates with the per-stream session ticket, on your own machine with
19
+ `neuralis login`; never with a git credential (fetch/pull/push resolve the remote token
20
+ server-side). The method is part of every call line: `commit` and `branch` READ on GET and
21
+ WRITE on POST. This is *not* about committing to the Neuralis repo itself — it is about an
22
+ agent operating git on a user's source.
21
23
 
22
24
  ## Three ways to reach git — pick the plane
23
25
 
24
26
  | Plane | Use it for | How | Guarantees |
25
27
  |---|---|---|---|
26
- | **A. Governed routes** (prefer for local ops + your own remote) | Review, stage, commit, branch/checkout, and fetch/pull/push on a git-backed source in the workspace | `bash ${SKILL_DIR}/scripts/git.sh …` (this skill) | uri-policy gated; auto-snapshot on destructive ops; remote token resolved server-side, never in the shell |
28
+ | **A. Governed routes** (prefer for local ops + your own remote) | Review, stage, commit, branch/checkout, and fetch/pull/push on a git-backed source in the workspace | `neuralis request <GET|POST> @neuralis/brain-core/git/<route>` (this skill) | uri-policy gated; auto-snapshot on destructive ops; remote token resolved server-side, never in the shell |
27
29
  | **B. External GitHub MCP** | Collaboration on the hosting platform — pull requests, issues, reviews, releases | the connected GitHub MCP server's tools (if the user connected one) | authz delegated to the GitHub account + enterprise settings — Neuralis does not re-implement git ACLs |
28
30
  | **C. Raw git CLI** | Quick local inspection, or a user's own imported git skill | `execute action="shell"` running `git …` | uri-policy + Landlock sandboxed; secrets are stripped from the env, so it **cannot push**, and there is **no server snapshot**. It also resolves the repository by walking UP from `cwd`, so it is **repository-scoped, never source-scoped** — see the `repo` parameter note below |
29
31
 
30
32
  Default to **A** for governed local ops and for pushing/pulling your connected remote; **B**
31
33
  for platform collaboration; **C** for a fast read or when a user's own skill already drives git.
32
- Plane A is always "call the routes via `git.sh`" — the first-party `git` connector shown in the
34
+ Plane A is always "call the routes with `neuralis request`" — the first-party `git` connector shown in the
33
35
  `<packages>` overview is metadata; there are no first-party git MCP *tools* to call.
34
36
 
35
37
  ## Routes covered
36
38
 
37
- Base path: `/api/packages/@neuralis/brain-core/git/<route>`. GET carries `source` (+ optional
38
- `repo`) as query params; POST carries them in the JSON body. `repo` is the optional in-source
39
+ Path: `@neuralis/brain-core/git/<route>`. GET carries `source` (+ optional `repo`) as `-f`
40
+ query fields; POST carries them in the JSON body (`--input -`), and every POST body must include
41
+ `"source"` plus that route's own required fields (e.g. `paths[]` for `stage`, `message` for
42
+ `commit`) — a source-less body is a `400`. `repo` is the optional in-source
39
43
  selector for a nested checkout when the source root is not itself the repo (omit for a
40
44
  single-repo source).
41
45
 
@@ -58,8 +62,10 @@ with nothing to report**. The same empty answer covers a
58
62
  source with no `.git` at all. (`commit`, `outgoing` and `show` are honest — they answer a typed
59
63
  `{"ok":false,"code":"unavailable"}` — and `diff` returns an empty diff string, except when it
60
64
  refuses a huge read with its own typed `{"ok":false,"code":"too_large"}`. Both of those typed
61
- refusals arrive on a **200**, so `git.sh` exits 0: a zero exit with `ok:false` is a soft answer,
62
- a non-zero exit is an HTTP refusal.)
65
+ refusals arrive on a **200**, so `neuralis request` exits 0: a zero exit with `ok:false` is a
66
+ soft answer, a non-zero exit is an HTTP refusal whose body and `HTTP <code>` are on stderr —
67
+ EVERY refusal carries `error`; a write-op failure and the three remote 409s also carry
68
+ `ok:false` and `code`; a 403 carries `code` (`feature_denied` / `uri_policy_denied`).)
63
69
 
64
70
  **So an empty read is never evidence of a clean tree.** Do not test it with `branch:null`: a real
65
71
  repository on a detached HEAD, or freshly initialised with no commit yet, reports exactly that.
@@ -112,7 +118,7 @@ Notes:
112
118
  `no_credential` / `no_https_remote` otherwise).
113
119
  - Destructive routes (`discard`, `branch` delete) snapshot server-side **before** mutating and
114
120
  return a typed `rescue` carrier. **Undo it through the governed `restore` route** — echo the
115
- carrier straight back: `POST restore '{"source":"…","rescue":{"kind":"stash","ref":"<sha>"}}'`
121
+ carrier straight back: `POST restore` with `{"source":"…","rescue":{"kind":"stash","ref":"<sha>"}}`
116
122
  for discarded content, `{"kind":"branch","ref":"<anchor>","name":"<branch>"}` for a deleted
117
123
  branch. Same gate as any other write (`drive.write` + `write` on the root — no `exec`, no new
118
124
  capability), and the restore itself snapshots first. Do **not** reach for raw-shell
@@ -153,21 +159,13 @@ connect the remote in that card first.
153
159
  general "don't `reset --hard` / `--no-verify` / force-push unrequested" rule still applies to
154
160
  everything not named here.
155
161
 
156
- ## Env vars
157
-
158
- - `NEURALIS_API` + `NEURALIS_SESSION_TOKEN` — injected at activation; every `git.sh` call reads them.
159
- - `git.sh` arguments: `<GET|POST> <route>` then, for **GET**, URL-encoded `key=value` pairs; for
160
- **POST**, a single JSON body string. Every POST body must include `"source"` plus that route's
161
- own required fields (e.g. `paths[]` for `stage`, `message` for `commit`) — a source-less body is
162
- a `400`.
163
-
164
162
  ## Examples
165
163
 
166
164
  ```bash
167
165
  # Review: what changed, then the diff of one file, then recent history
168
- bash ${SKILL_DIR}/scripts/git.sh GET status source=my-repo
169
- bash ${SKILL_DIR}/scripts/git.sh GET diff source=my-repo "path=src/app.ts"
170
- bash ${SKILL_DIR}/scripts/git.sh GET log source=my-repo maxCount=10
166
+ neuralis request GET @neuralis/brain-core/git/status -f source=my-repo
167
+ neuralis request GET @neuralis/brain-core/git/diff -f source=my-repo -f path=src/app.ts
168
+ neuralis request GET @neuralis/brain-core/git/log -f source=my-repo -f maxCount=10
171
169
 
172
170
  # NOT DISPOSABLE — every POST below writes to a REAL repository. These routes only
173
171
  # operate on an already-attached git-backed source, so there is no throwaway repo to
@@ -181,16 +179,16 @@ bash ${SKILL_DIR}/scripts/git.sh GET log source=my-repo maxCount=10
181
179
  # authorship, and is unsafe once it has been pushed (the remote keeps the original
182
180
  # and the two histories diverge). Stage NAMED paths only — `paths:[…]` is what keeps
183
181
  # a stray secret from being swept in; there is no route that un-commits.
184
- bash ${SKILL_DIR}/scripts/git.sh POST stage '{"source":"my-repo","paths":["src/app.ts","README.md"]}'
185
- bash ${SKILL_DIR}/scripts/git.sh POST commit '{"source":"my-repo","message":"Fix null guard in loader"}'
182
+ neuralis request POST @neuralis/brain-core/git/stage --input - <<<'{"source":"my-repo","paths":["src/app.ts","README.md"]}'
183
+ neuralis request POST @neuralis/brain-core/git/commit --input - <<<'{"source":"my-repo","message":"Fix null guard in loader"}'
186
184
 
187
185
  # Create a branch, then switch to it.
188
186
  # checkout MOVES HEAD and swaps the working tree to that branch's content — the files
189
187
  # the user is looking at change under them. It is plain (never `-f`): if the switch
190
188
  # would overwrite uncommitted local changes, git refuses and you get a 409. Report
191
189
  # that refusal; there is no force option here and you must not build one in Plane C.
192
- bash ${SKILL_DIR}/scripts/git.sh POST branch '{"source":"my-repo","action":"create","name":"fix/loader"}'
193
- bash ${SKILL_DIR}/scripts/git.sh POST checkout '{"source":"my-repo","ref":"fix/loader"}'
190
+ neuralis request POST @neuralis/brain-core/git/branch --input - <<<'{"source":"my-repo","action":"create","name":"fix/loader"}'
191
+ neuralis request POST @neuralis/brain-core/git/checkout -f source=my-repo -f ref=fix/loader
194
192
 
195
193
  # Sync a connected remote (needs uri-policy exec + a connected remote).
196
194
  # pull REWRITES the working tree to the remote tip — fast-forward only, so it refuses
@@ -200,9 +198,9 @@ bash ${SKILL_DIR}/scripts/git.sh POST checkout '{"source":"my-repo","ref":"fix/l
200
198
  # OUTWARD-FACING: teammates, CI, and anything deploying from that branch see it the
201
199
  # moment it lands, and nothing in Neuralis can take it back. ASK THE USER FIRST, every
202
200
  # time — this is the one line here you never run on your own initiative.
203
- bash ${SKILL_DIR}/scripts/git.sh POST pull '{"source":"my-repo"}'
204
- bash ${SKILL_DIR}/scripts/git.sh POST push '{"source":"my-repo"}'
201
+ neuralis request POST @neuralis/brain-core/git/pull -f source=my-repo
202
+ neuralis request POST @neuralis/brain-core/git/push -f source=my-repo
205
203
 
206
204
  # Nested repo inside a multi-repo source: add the repo selector
207
- bash ${SKILL_DIR}/scripts/git.sh GET status source=mono repo=packages/api
205
+ neuralis request GET @neuralis/brain-core/git/status -f source=mono -f repo=packages/api
208
206
  ```
@@ -7,24 +7,25 @@ allowed-tools: execute
7
7
 
8
8
  # Files Management Skill
9
9
 
10
- Wraps `/api/packages/@neuralis/brain-core/{list,search,read,raw,write,upload,approve,pending}`
11
- behind one shell-callable bundle. The `fs_*` tools (`fs_read`, `fs_write`,
12
- etc.) remain the primary path for one-off file operations — this bundle is
13
- for *bulk*: iterating with a shell loop, posting a batch, or scripting an
14
- `upload + approve` sequence end-to-end.
10
+ Covers `@neuralis/brain-core/{list,search,read,raw,write,upload,approve,pending}`.
11
+ The `fs_*` tools (`fs_read`, `fs_write`, etc.) remain the primary path for one-off
12
+ file operations — this skill is for *bulk*: iterating with a shell loop, posting a
13
+ batch, or scripting an `upload + approve` sequence end-to-end. Call the routes with
14
+ `neuralis request` (the session ticket inside an activated skill, `neuralis login`
15
+ on your own machine); `write` and `upload` go through their scripts.
15
16
 
16
17
  ## Routes covered
17
18
 
18
- | Action | Method | Path | Script | Feature gate |
19
- |---|---|---|---|---|
20
- | List files / folders at a URI | POST | `/list` | `list.sh` | `drive.read` |
21
- | Search vector index | POST | `/search` | `search.sh` | `drive.search` |
22
- | Read multiple files | POST | `/read` | `read.sh` | `drive.read` |
23
- | Raw (un-summarized) read | GET | `/raw` | `raw.sh` | `drive.read` |
24
- | Write — every verb (create / overwrite / edit / insert / lines / fields / move / copy / mkdir / revert / delete) | POST | `/write` | `write.sh` | `drive.write` |
25
- | Upload a payload (multipart/form-data) | POST | `/upload` | `upload.sh` | `drive.write` |
26
- | Approve a pending change | POST | `/approve` | `approve.sh` | `drive.write` |
27
- | List pending changes (project-wide) | GET | `/pending` | `pending.sh` | `drive.read` |
19
+ | Action | Method | Path (`@neuralis/brain-core/…`) | Feature gate |
20
+ |---|---|---|---|
21
+ | List files / folders at a URI | POST | `list` | `drive.read` |
22
+ | Search vector index | POST | `search` | `drive.search` |
23
+ | Read multiple files | POST | `read` | `drive.read` |
24
+ | Raw (un-summarized) read | GET | `raw` | `drive.read` |
25
+ | Write — every verb (create / overwrite / edit / insert / lines / fields / move / copy / mkdir / revert / delete) | POST | `write` — `scripts/write.sh` | `drive.write` |
26
+ | Upload a payload (multipart/form-data) | POST | `upload` — `scripts/upload.sh` | `drive.write` |
27
+ | Approve a pending change | POST | `approve` | `drive.write` |
28
+ | List pending changes (project-wide) | GET | `pending` | `drive.read` |
28
29
 
29
30
  Per-route feature gates are enforced server-side by the RouteDispatcher against the
30
31
  caller's real `grantedFeatures`, independently of this skill's `requiredFeatures`
@@ -32,41 +33,45 @@ frontmatter — `/list`, `/read`, `/raw` and `GET /pending` need `drive.read`; `
32
33
  `drive.search`; `/write`, `/upload`, `/approve` and the mutating `/pending`
33
34
  verbs (`revert`, `discard`, `dismiss`, `discard-orphans`) need `drive.write`. A caller who holds
34
35
  `drive.write` (enough to see and activate this skill) but NOT `drive.search` gets a
35
- server-side **403** from `search.sh` even though the other scripts work.
36
+ server-side **403** from `search` even though the other routes work.
36
37
 
37
- ## Env vars
38
+ ## Bodies
38
39
 
39
- - All scripts read `NEURALIS_API` + `NEURALIS_SESSION_TOKEN` (both injected by the skill activation; the token is a per-stream session ticket).
40
- - `BODY_JSON` — the JSON request body for `list` / `search` / `read` / `write` / `approve`; on `pending.sh` it selects POST instead of GET. `create.sh`, `modify.sh`, `copy.sh` and `mkdir.sh` are kept only so an imported skill's existing path still writes; they exec `write.sh`, so they take the same verb-keyed body.
41
- - `/write` body = `{"agentId":"…","uri":"data://notes/plan.md","<verb>":…}` — the VERB is a KEY and its presence is the intent. Content verbs COMPOSE in one call, in a fixed order, as ONE write with ONE baseline: `create` (`{content, client_request_id?}` — never overwrites; a taken name lands BESIDE as `name-2.ext` and the response `uri` is where it went, so read it back), `overwrite` (`{content}` — refuses on a missing file and names `create`), `edit` (`[{old_string, new_string?, replace_all?, expected_replacements?}]`, applied in order, one failure discards all of them), `insert` (`{insert_text, insert_line?}` — inserts AFTER that line, `0` = start of file, omitted = append), `lines` (`{line_start, line_end, content}`), `fields` (`{category, status, pinned, tags{add,remove,set}, metadata}`). Structural verbs stand ALONE: `move`/`copy` (a destination uri or, for a file move, a bare new filename; `copy` may cross sources), `mkdir:true` (a real empty directory; not on `brain://`, where folders are implicit), `revert:true` (one-level undo), `delete:true` (a file, or a whole folder recursively). **A 2xx does not mean success** — a refusal is 200 with `ok:false`, so branch on `ok`. A `move` (or a `revert` that relocates or re-creates) onto a uri that already holds something answers `ok:false` + `error:"DESTINATION_EXISTS"` — nothing was overwritten; delete or rename the occupant first. A delete has ONE spelling, the `delete` key: `fields.status:"deleted"` is refused. `review` is never read from the body — a content edit (`overwrite`/`edit`/`insert`/`lines`) lands `pending_review` on every lane (visible and revertible in Changes); `create`/`move`/`copy`/`mkdir` land committed for a human interactive caller and `pending_review` from this ticket lane; `/upload` also journals agent-origin uploads as pending review; interactive human uploads land committed. Use `/write` for JSON text and `/upload` for multipart file bytes. A missing `agentId`/`uri` is a **400** naming the field, a body the server could not parse a **400** `Request body must be a JSON object`. A uri-policy denial is a **403 with no `ok` field**, a disabled source a **409** with `code:"source_disabled"`. A source slug this project does not have answers one of two ways, and BOTH mean "stop guessing this name": with a scheme (`zzz://a.md`) the uri gate answers **403 `uri_policy_denied`** first; without one (`notes.md`) you get **404 `code:"source_not_configured"`** carrying `availableSources` — the sources you CAN address. `/list` gives the 404 in both forms. Switch to a source from that list (or from `<runtime_stack>`); retrying the same name never works. `/read` is different again: it reports PER FILE inside a 200, so a denied or disabled source arrives as `{uri, error, code:"forbidden"}` — never a 409 — and a missing file as `code:"not_found"`. No code means the read failed for some other reason; do not conclude the file is gone. A folder delete or copy that partly succeeded reports what really happened: `affected` (files touched) and `failed` (the NAMES that could not be), so "failed" does not mean "nothing happened". `warning:"moved_with_source_retained"` on a move means the copy landed but the original could not be removed — BOTH copies exist on purpose; do not re-run the move. `indexed:false` means the bytes DID land and are readable, but search indexing was skipped or failed; report it, do not re-write. Sync retries failed indexing only for eligible files; excluded files remain unindexed. Every script here runs curl with `--fail-with-body`, so a non-2xx prints that body too — in the merged output the body comes FIRST and curl's own `curl: (22)` line after it, under a `--- stderr ---` separator, so read the whole output.
40
+ - `list` / `search` / `read` / `approve` take a JSON body (`--input - <<<'<json>'`);
41
+ `write.sh` takes it as `BODY_JSON`. `create.sh`, `modify.sh`, `copy.sh` and `mkdir.sh` are kept only so an imported skill's existing path still writes; they exec `write.sh`, so they take the same verb-keyed body.
42
+ - `/write` body = `{"agentId":"…","uri":"data://notes/plan.md","<verb>":…}` — the VERB is a KEY and its presence is the intent. Content verbs COMPOSE in one call, in a fixed order, as ONE write with ONE baseline: `create` (`{content, client_request_id?}` — never overwrites; a taken name lands BESIDE as `name-2.ext` and the response `uri` is where it went, so read it back), `overwrite` (`{content}` — refuses on a missing file and names `create`), `edit` (`[{old_string, new_string?, replace_all?, expected_replacements?}]`, applied in order, one failure discards all of them), `insert` (`{insert_text, insert_line?}` — inserts AFTER that line, `0` = start of file, omitted = append), `lines` (`{line_start, line_end, content}`), `fields` (`{category, status, pinned, tags{add,remove,set}, metadata}`). Structural verbs stand ALONE: `move`/`copy` (a destination uri or, for a file move, a bare new filename; `copy` may cross sources), `mkdir:true` (a real empty directory; not on `brain://`, where folders are implicit), `revert:true` (one-level undo), `delete:true` (a file, or a whole folder recursively). **A 2xx does not mean success** — a refusal is 200 with `ok:false`, so branch on `ok`. A `move` (or a `revert` that relocates or re-creates) onto a uri that already holds something answers `ok:false` + `error:"DESTINATION_EXISTS"` — nothing was overwritten; delete or rename the occupant first. A delete has ONE spelling, the `delete` key: `fields.status:"deleted"` is refused. `review` is never read from the body — a content edit (`overwrite`/`edit`/`insert`/`lines`) lands `pending_review` on every lane (visible and revertible in Changes); `create`/`move`/`copy`/`mkdir` land committed for a human interactive caller and `pending_review` from this ticket lane; `/upload` also journals agent-origin uploads as pending review; interactive human uploads land committed. Use `/write` for JSON text and `/upload` for multipart file bytes. A missing `agentId`/`uri` is a **400** naming the field, a body the server could not parse a **400** `Request body must be a JSON object`. A uri-policy denial is a **403 with no `ok` field**, a disabled source a **409** with `code:"source_disabled"`. A source slug this project does not have answers one of two ways, and BOTH mean "stop guessing this name": with a scheme (`zzz://a.md`) the uri gate answers **403 `uri_policy_denied`** first; without one (`notes.md`) you get **404 `code:"source_not_configured"`** carrying `availableSources` — the sources you CAN address. `/list` gives the 404 in both forms. Switch to a source from that list (or from `<runtime_stack>`); retrying the same name never works. `/read` is different again: it reports PER FILE inside a 200, so a denied or disabled source arrives as `{uri, error, code:"forbidden"}` — never a 409 — and a missing file as `code:"not_found"`. No code means the read failed for some other reason; do not conclude the file is gone. A folder delete or copy that partly succeeded reports what really happened: `affected` (files touched) and `failed` (the NAMES that could not be), so "failed" does not mean "nothing happened". `warning:"moved_with_source_retained"` on a move means the copy landed but the original could not be removed — BOTH copies exist on purpose; do not re-run the move. `indexed:false` means the bytes DID land and are readable, but search indexing was skipped or failed; report it, do not re-write. Sync retries failed indexing only for eligible files; excluded files remain unindexed. A non-2xx prints the route's body too — on stderr, followed by an `HTTP <code>` line, under a `--- stderr ---` separator, so read the whole output.
42
43
  - `/search` body key is `search_mode` (semantic|filter|grep) — the SAME vocabulary as the `fs_search` tool; the legacy `mode` key is rejected with 400. The route forwards `search_mode`, `query`, `folder_uri`, `glob`, `category`, `tags`, `status`, `created_after`/`created_before`, `limit`, and the grep-only `path_glob`, `context` and `case_sensitive`. In `grep` mode the search is uri-policy `read`-gated (only paths the caller holds `read` policy on are grepped), the match is a FIXED STRING, and the answer carries `truncated` + `total_seen` — a cut set says so, so never read a full-looking list as proof that the rest is absent.
43
- - `/raw` is a GET — pass the file URI via `URI=…` (no `BODY_JSON`). It always returns the full bytes, so `raw.sh` is unaffected by the response headers; what those headers say is a BROWSER instruction. Safe types (images except SVG, video, audio, PDF, text, JSON) are marked for inline display; SVG, HTML and XML are marked `attachment` so a person clicking the link downloads them instead of executing them on the app origin. Do not describe a `.svg` or `.html` you wrote as viewable in the browser — offer it as a download, or convert it to a raster image if a human needs to look at it.
44
+ - `/raw` is a GET — pass the file URI as `-f uri=<uri>` and redirect stdout to a file: it always returns the full bytes, unaffected by the response headers; what those headers say is a BROWSER instruction. Safe types (images except SVG, video, audio, PDF, text, JSON) are marked for inline display; SVG, HTML and XML are marked `attachment` so a person clicking the link downloads them instead of executing them on the app origin. Do not describe a `.svg` or `.html` you wrote as viewable in the browser — offer it as a download, or convert it to a raster image if a human needs to look at it.
44
45
  - `/read` is at parity with the `fs_read` tool: the body takes `files[]` — each entry a URI string or `{"uri":"…","line_start":N,"line_end":M}` with that file's own window — plus `read_version`, batch-level `line_start`/`line_end` and `include_ids`. An entry that is neither shape is a **400**. Text is a bounded preview; native media may accompany it, while unsupported/invalid binary data has a URI and marker. For a byte-exact binary copy use the authorized `/raw` byte lane and multipart `/upload`, never re-encode a text preview. If you only need the pre-edit content, a plain `/read` response already carries it: each file's `version.baseline.content`, with `available_versions` listing `baseline` exactly when one exists.
45
- - `/upload` is **multipart/form-data**, NOT JSON — pass `FILE_PATH=…` `AGENT_ID=…` (required) plus optional `URI=…` / `TAGS=a,b`; a JSON body is rejected with 400. The 200 carries `indexed`: `false` means the bytes DID land but search indexing was skipped by the source selection or failed (provider outage/quota) — report it, do not re-upload. An eligible sync retries failed indexing; excluded uploads remain unindexed until the selection changes. The 200 also carries `mimeType`, and it may differ from what the upload declared: the stored type is resolved from the filename extension first, with the declared value kept only for an extension the platform does not know, and a common-image claim the leading bytes contradict falls back to `application/octet-stream`. Give the file a truthful extension and the stored type follows.
46
- - `GET /pending` lists durable pending records inside the Changes workspace. `STATUS`, `SOURCE`, `OP`, and `LIMIT` filter the list; owner/scope and URI READ gates apply per record. Review begins with `POST /pending` `{action:"preview",id}`: it returns the current connector content separately from the recorded snapshot and saved baseline, typed missing/unavailable/binary/too-large states, `indexState`, `requiresAcknowledgement`, and `expectedFingerprint`. The preview is bounded by `pendingPreviewMaxBytes`; it never fabricates missing content or vectors. Resolve through `POST /approve` `{id,expectedFingerprint}` or `POST /pending` `{action:"revert"|"dismiss",id,expectedFingerprint}`. If the preview requires acknowledgement, inspect those states and include `acknowledgeCurrentState:true`; never auto-acknowledge an unseen preview. The server rechecks freshness and WRITE authority at apply: a changed fingerprint requires a new preview, and acknowledgement grants no missing permission. Bare legacy `/write revert:true` refuses unsafe pending recovery; ordinary non-pending one-level undo remains available. Approval/dismissal preserve the stored snapshot/index state (including recovery-only or absent index), rather than certifying current connector bytes as indexed. For a file the preview shows `missing` with a saved `journal-latest-write` baseline, an acknowledged approve/dismiss closes the review and the file stays gone; revert is the restore. Terminal retries remain owner-gated and idempotent; `uri` is omitted without current READ and WRITE access. Revert restores the saved baseline and refuses missing baselines or occupied move destinations. Discard/Repair remain orphan-only, owner-gated operations; no transient outage is proof of orphanhood.
46
+ - `/upload` is **multipart/form-data**, NOT JSON — `upload.sh` takes `FILE_PATH=…` `AGENT_ID=…` (required) plus optional `URI=…` / `TAGS=a,b`; a JSON body is rejected with 400. The 200 carries `indexed`: `false` means the bytes DID land but search indexing was skipped by the source selection or failed (provider outage/quota) — report it, do not re-upload. An eligible sync retries failed indexing; excluded uploads remain unindexed until the selection changes. The 200 also carries `mimeType`, and it may differ from what the upload declared: the stored type is resolved from the filename extension first, with the declared value kept only for an extension the platform does not know, and a common-image claim the leading bytes contradict falls back to `application/octet-stream`. Give the file a truthful extension and the stored type follows.
47
+ - `GET /pending` lists durable pending records inside the Changes workspace. `status` (`pending_review`\|`approved`\|`reverted`), `source`, `op` (`create`\|`modify`\|`delete`\|`move`) and `limit` filter the list; owner/scope and URI READ gates apply per record. Review begins with `POST /pending` `{action:"preview",id}`: it returns the current connector content separately from the recorded snapshot and saved baseline, typed missing/unavailable/binary/too-large states, `indexState`, `requiresAcknowledgement`, and `expectedFingerprint`. The preview is bounded by `pendingPreviewMaxBytes`; it never fabricates missing content or vectors. Resolve through `POST /approve` `{id,expectedFingerprint}` or `POST /pending` `{action:"revert"|"dismiss",id,expectedFingerprint}`. If the preview requires acknowledgement, inspect those states and include `acknowledgeCurrentState:true`; never auto-acknowledge an unseen preview. The server rechecks freshness and WRITE authority at apply: a changed fingerprint requires a new preview, and acknowledgement grants no missing permission. Bare legacy `/write revert:true` refuses unsafe pending recovery; ordinary non-pending one-level undo remains available. Approval/dismissal preserve the stored snapshot/index state (including recovery-only or absent index), rather than certifying current connector bytes as indexed. For a file the preview shows `missing` with a saved `journal-latest-write` baseline, an acknowledged approve/dismiss closes the review and the file stays gone; revert is the restore. Terminal retries remain owner-gated and idempotent; `uri` is omitted without current READ and WRITE access. Revert restores the saved baseline and refuses missing baselines or occupied move destinations. Discard/Repair remain orphan-only, owner-gated operations; no transient outage is proof of orphanhood.
47
48
 
48
49
 
49
- The body shape is route-specific — refer to brain-core's
50
- `/api/packages/@neuralis/brain-core/list,search,read,write,…` route source
51
- for the exact schemas. For one-off calls, prefer the `fs_*` tools.
50
+ For one-off calls, prefer the `fs_*` tools.
52
51
 
53
52
  ## Examples
54
53
 
55
54
  For a disposable pending record you created in your project, substitute its real id below. Inspect the preview before resolving; copy its exact fingerprint, and add acknowledgement only after reviewing the states the preview names. A 409 requires a new preview, never a fabricated fingerprint.
56
55
 
57
56
  ```bash
58
- BODY_JSON='{"action":"preview","id":"<scratch-record-id>"}' bash ${SKILL_DIR}/scripts/pending.sh
59
- BODY_JSON='{"action":"revert","id":"<scratch-record-id>","expectedFingerprint":"<64-hex-preview-fingerprint>","acknowledgeCurrentState":true}' bash ${SKILL_DIR}/scripts/pending.sh
57
+ neuralis request POST @neuralis/brain-core/pending -f action=preview -f id=<scratch-record-id>
58
+ neuralis request POST @neuralis/brain-core/pending --input - <<<'{"action":"revert","id":"<scratch-record-id>","expectedFingerprint":"<64-hex-preview-fingerprint>","acknowledgeCurrentState":true}'
60
59
  ```
61
60
 
61
+ Pending calls return one page; scan-orphans preserves that page in `changes` and puts its orphan subset in `orphans`/`orphanIds`: pass `cursor` from `nextCursor` unchanged with the same filters, or `uri` for an exact URI. Check `complete`, `partial` and `hasMoreAvailable`; a partial exhausted branch remains incomplete. On `restartRequired`, restart the list instead of retrying the old cursor. Repair returns scannedVisible/orphaned/attempted/discarded/refused/failed/reasons for one batch; continue only via nextCursor; pause on partial and use an explicit Continue available/Retry.
62
+
62
63
  ```bash
63
64
  # Recursive list of /data
64
- BODY_JSON='{"uri":"data://","recursive":true,"limit":100}' \
65
- bash ${SKILL_DIR}/scripts/list.sh
65
+ neuralis request POST @neuralis/brain-core/list --input - <<<'{"uri":"data://","recursive":true,"limit":100}'
66
66
 
67
67
  # Vector search across all sources (body key is `search_mode`, same as the fs_search tool)
68
- BODY_JSON='{"search_mode":"semantic","query":"telemetry pipeline","limit":10}' \
69
- bash ${SKILL_DIR}/scripts/search.sh
68
+ neuralis request POST @neuralis/brain-core/search -f search_mode=semantic -f query='telemetry pipeline' -F limit=10
69
+
70
+ # Pending review records still waiting, one page
71
+ neuralis request GET @neuralis/brain-core/pending -f status=pending_review -f limit=20
72
+
73
+ # The exact bytes of one file
74
+ neuralis request GET @neuralis/brain-core/raw -f uri=data://report.pdf > /tmp/report.pdf
70
75
 
71
76
  # Create a text file — the journaled write lane. The verb is the KEY.
72
77
  # `<agent-id>` is a placeholder; a real id makes this line write for real.
@@ -1,7 +1,5 @@
1
1
  #!/usr/bin/env bash
2
2
  set -euo pipefail
3
- API_BASE="${NEURALIS_API:?NEURALIS_API not set; activate skill first}"
4
- TOKEN="${NEURALIS_SESSION_TOKEN:?missing session ticket; activate the skill via execute action=\"skill\"}"
5
3
  # POST /upload REQUIRES multipart/form-data (NOT a JSON body): the handler reads the
6
4
  # form fields file (the binary/text payload), agentId, uri (optional), tags (optional,
7
5
  # comma-split). projectId is taken from the session ticket — passing a different one
@@ -9,10 +7,10 @@ TOKEN="${NEURALIS_SESSION_TOKEN:?missing session ticket; activate the skill via
9
7
  # from the form field.
10
8
  FILE_PATH="${FILE_PATH:?FILE_PATH is required (local path of the payload to upload)}"
11
9
  AGENT_ID="${AGENT_ID:?AGENT_ID is required (form field; the destination agent)}"
12
- curl --fail-with-body -sS -X POST \
13
- -H "Authorization: Bearer ${TOKEN}" -H "X-Neuralis-Skill: manage-files" \
14
- -F "file=@${FILE_PATH}" \
15
- -F "agentId=${AGENT_ID}" \
16
- ${URI:+-F "uri=${URI}"} \
17
- ${TAGS:+-F "tags=${TAGS}"} \
18
- "${API_BASE}/api/packages/@neuralis/brain-core/upload"
10
+ # `--form` sends multipart/form-data (`file=@path` is the file part). A non-2xx
11
+ # answer prints the route's body and `HTTP <code>` on stderr and exits 1.
12
+ neuralis request POST @neuralis/brain-core/upload \
13
+ --form "file=@${FILE_PATH}" \
14
+ --form "agentId=${AGENT_ID}" \
15
+ ${URI:+--form "uri=${URI}"} \
16
+ ${TAGS:+--form "tags=${TAGS}"}
@@ -16,8 +16,6 @@ set -euo pipefail
16
16
  # `edit`, `insert`, `lines`) lands `pending_review` on EVERY lane; `create`, `move`,
17
17
  # `copy` and `mkdir` land committed for a human interactive caller and
18
18
  # `pending_review` from this ticket lane; `delete` tombstones on both.
19
- API_BASE="${NEURALIS_API:?NEURALIS_API not set; activate skill first}"
20
- TOKEN="${NEURALIS_SESSION_TOKEN:?missing session ticket; activate the skill via execute action=\"skill\"}"
21
19
  BODY_JSON="${BODY_JSON:?BODY_JSON is required — a JSON object with agentId, uri and ONE verb key; see the header comment for the shape}"
22
20
  # The retired body shape names no verb; the route would only say "Missing uri".
23
21
  # Refuse it HERE, naming what the caller has to send instead.
@@ -27,7 +25,5 @@ case "${BODY_JSON}" in
27
25
  exit 2
28
26
  ;;
29
27
  esac
30
- curl --fail-with-body -sS -X POST \
31
- -H "Authorization: Bearer ${TOKEN}" -H "X-Neuralis-Skill: manage-files" \
32
- -H "Content-Type: application/json" -d "${BODY_JSON}" \
33
- "${API_BASE}/api/packages/@neuralis/brain-core/write"
28
+ # A non-2xx answer prints the route's body and `HTTP <code>` on stderr and exits 1.
29
+ neuralis request POST @neuralis/brain-core/write --input - <<<"${BODY_JSON}"