@north-light/crouter 0.3.217 → 0.3.219
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/dist/api/client.d.ts +5 -2
- package/dist/api/client.js +7 -1
- package/dist/api/dto/canvas.d.ts +9 -0
- package/dist/api/dto/nodes.d.ts +27 -1
- package/dist/builtin-memory/04-base-worker.md +7 -1
- package/dist/builtin-memory/internal/plugins.md +67 -0
- package/dist/builtin-memory/memory-read-orientation.md +13 -0
- package/dist/clients/attach/chrome/bash-jobs.js +1 -1
- package/dist/clients/attach/photon_rs_bg.wasm +0 -0
- package/dist/clients/attach/viewer.js +478 -478
- package/dist/commands/human/shared.js +1 -1
- package/dist/commands/memory/edit.js +6 -1
- package/dist/commands/memory/lint.d.ts +1 -6
- package/dist/commands/memory/lint.js +18 -126
- package/dist/commands/memory/list.d.ts +1 -0
- package/dist/commands/memory/list.js +17 -2
- package/dist/commands/memory/read.js +15 -6
- package/dist/commands/memory/shared.d.ts +32 -2
- package/dist/commands/memory/shared.js +179 -0
- package/dist/commands/memory/write.js +6 -1
- package/dist/commands/pkg/browse/catalog.js +2 -0
- package/dist/commands/pkg/browse/model.d.ts +4 -1
- package/dist/commands/pkg/plugin-manage.js +209 -148
- package/dist/core/bash-jobs.d.ts +2 -5
- package/dist/core/bash-jobs.js +4 -8
- package/dist/core/command-plugins/bundle.d.ts +4 -1
- package/dist/core/command-plugins/bundle.js +16 -2
- package/dist/core/human/component-docs.js +1 -0
- package/dist/core/human/scan.d.ts +5 -4
- package/dist/core/human/scan.js +7 -4
- package/dist/core/io.js +3 -2
- package/dist/core/manifest.d.ts +2 -0
- package/dist/core/manifest.js +5 -0
- package/dist/core/memory/extensions.d.ts +30 -0
- package/dist/core/memory/extensions.js +219 -0
- package/dist/core/preview-result-path.d.ts +4 -0
- package/dist/core/preview-result-path.js +25 -0
- package/dist/core/runtime/launch-config-worker.d.ts +1 -0
- package/dist/core/runtime/launch-config-worker.js +12 -0
- package/dist/core/runtime/launch.d.ts +16 -2
- package/dist/core/runtime/launch.js +55 -21
- package/dist/core/runtime/promote.d.ts +20 -2
- package/dist/core/runtime/promote.js +63 -59
- package/dist/core/runtime/recycle.js +2 -2
- package/dist/core/runtime/reset.js +2 -2
- package/dist/core/runtime/spawn.js +3 -3
- package/dist/core/runtime/warm-pool.js +9 -7
- package/dist/core/substrate/frontmatter-validation.d.ts +13 -0
- package/dist/core/substrate/frontmatter-validation.js +101 -0
- package/dist/core/substrate/index.d.ts +1 -0
- package/dist/core/substrate/index.js +1 -0
- package/dist/daemon/api/__tests__/bridge-heartbeat.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/bridge-heartbeat.test.js +30 -0
- package/dist/daemon/api/__tests__/nodes-activity-query.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/nodes-activity-query.test.js +101 -0
- package/dist/daemon/api/__tests__/seam/api-server.test.js +30 -0
- package/dist/daemon/api/bridge.d.ts +6 -0
- package/dist/daemon/api/bridge.js +39 -2
- package/dist/daemon/api/handlers/canvas.js +3 -0
- package/dist/daemon/api/handlers/inbox.js +4 -3
- package/dist/daemon/api/handlers/nodes.d.ts +1 -0
- package/dist/daemon/api/handlers/nodes.js +117 -36
- package/dist/daemon/api/handlers/reports.d.ts +4 -0
- package/dist/daemon/api/handlers/reports.js +14 -8
- package/dist/daemon/crtrd.js +7 -5
- package/dist/daemon/manage.d.ts +3 -0
- package/dist/daemon/manage.js +14 -0
- package/dist/pi-extensions/canvas-bash-valve.d.ts +4 -3
- package/dist/pi-extensions/canvas-bash-valve.js +19 -6
- package/dist/pi-extensions/canvas-preview-result.d.ts +0 -8
- package/dist/pi-extensions/canvas-preview-result.js +9 -23
- package/dist/types.d.ts +31 -0
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
package/dist/api/client.d.ts
CHANGED
|
@@ -78,6 +78,8 @@ export declare class CrtrClient {
|
|
|
78
78
|
* settled result. */
|
|
79
79
|
restartDaemon(): Promise<DaemonRestartDTO>;
|
|
80
80
|
createNode(req: CreateNodeRequest): Promise<NodeDetailDTO>;
|
|
81
|
+
/** List canvas nodes with composable row filters. `include: 'activity'` adds
|
|
82
|
+
* latest/canonical reports and pending-human counts in the same response. */
|
|
81
83
|
listNodes(q?: ListNodesQuery): Promise<NodeSummaryDTO[]>;
|
|
82
84
|
getNode(id: string): Promise<NodeDetailDTO>;
|
|
83
85
|
listBashJobs(id: string): Promise<BashJobStatusDTO[]>;
|
|
@@ -197,8 +199,9 @@ export declare class CrtrClient {
|
|
|
197
199
|
listHumanInbox(): Promise<InboxListDTO>;
|
|
198
200
|
/** Read one page ticket by its opaque ticket id (pending, resolved, or canceled). */
|
|
199
201
|
getHumanInboxPage(ticketId: InboxTicketIdDTO): Promise<InboxPageDTO>;
|
|
200
|
-
/** All page tickets
|
|
201
|
-
|
|
202
|
+
/** All page tickets, oldest first (pending, resolved, or canceled) —
|
|
203
|
+
* home-wide, or narrowed to one raising node when `nodeId` is given. */
|
|
204
|
+
getInboxHistory(nodeId?: string): Promise<InboxPageHistoryDTO>;
|
|
202
205
|
/** Submit responses for a page ticket. Single-assignment server-side: a competing
|
|
203
206
|
* resolution races to `ticket_already_resolved`. */
|
|
204
207
|
respondHumanInboxPage(ticketId: InboxTicketIdDTO, request: RespondInboxPageRequest): Promise<PageTicketResultDTO>;
|
package/dist/api/client.js
CHANGED
|
@@ -92,6 +92,8 @@ export class CrtrClient {
|
|
|
92
92
|
createNode(req) {
|
|
93
93
|
return this.request('POST', routes.nodes(), req);
|
|
94
94
|
}
|
|
95
|
+
/** List canvas nodes with composable row filters. `include: 'activity'` adds
|
|
96
|
+
* latest/canonical reports and pending-human counts in the same response. */
|
|
95
97
|
listNodes(q) {
|
|
96
98
|
return this.request('GET', withQuery(routes.nodes(), q));
|
|
97
99
|
}
|
|
@@ -386,8 +388,12 @@ export class CrtrClient {
|
|
|
386
388
|
getHumanInboxPage(ticketId) {
|
|
387
389
|
return this.request('GET', routes.humanInboxTicket(this.ticketId(ticketId)));
|
|
388
390
|
}
|
|
389
|
-
/** All page tickets
|
|
391
|
+
/** All page tickets, oldest first (pending, resolved, or canceled) —
|
|
392
|
+
* home-wide, or narrowed to one raising node when `nodeId` is given. */
|
|
390
393
|
getInboxHistory(nodeId) {
|
|
394
|
+
if (nodeId === undefined) {
|
|
395
|
+
return this.request('GET', routes.humanInbox() + '/history');
|
|
396
|
+
}
|
|
391
397
|
if (typeof nodeId !== 'string' || nodeId === '') {
|
|
392
398
|
throw new TypeError(`invalid node id: ${JSON.stringify(nodeId)}`);
|
|
393
399
|
}
|
package/dist/api/dto/canvas.d.ts
CHANGED
|
@@ -147,6 +147,10 @@ export interface SnapshotNodeDTO {
|
|
|
147
147
|
lifecycle?: string;
|
|
148
148
|
status: NodeStatusDTO;
|
|
149
149
|
cwd: string;
|
|
150
|
+
/** The profile this node runs under; null for a historical no-profile row.
|
|
151
|
+
* Carried here because it is the only field that says WHOSE work a node is —
|
|
152
|
+
* a snapshot consumer grouping nodes by agent has nothing else to key on. */
|
|
153
|
+
profile_id: string | null;
|
|
150
154
|
parent: NodeIdDTO | null;
|
|
151
155
|
created: IsoTime;
|
|
152
156
|
host_kind: string;
|
|
@@ -161,6 +165,11 @@ export interface SnapshotNodeDTO {
|
|
|
161
165
|
* as a loose object to keep this DTO free of a `core` type import. */
|
|
162
166
|
hanging: Record<string, unknown> | null;
|
|
163
167
|
viewed: boolean;
|
|
168
|
+
/** Basename of the node's final report, or null when it never pushed one.
|
|
169
|
+
* Presence — not `status === 'done'` — is what distinguishes finished work
|
|
170
|
+
* from clean parking. */
|
|
171
|
+
final_report: string | null;
|
|
172
|
+
finalized_at: IsoTime | null;
|
|
164
173
|
}
|
|
165
174
|
/** `GET /v1/canvas/snapshot` result — the machine-readable browser canvas
|
|
166
175
|
* roster (`crtr canvas snapshot`). Distinct from the per-node `NodeSnapshotDTO`
|
package/dist/api/dto/nodes.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Cursor, ExitIntentDTO, IsoTime, LifecycleDTO, ModeDTO, NodeIdDTO, NodeStatusDTO } from './common.js';
|
|
2
|
+
import type { ReportDTO } from './reports.js';
|
|
2
3
|
/** `GET /v1/nodes/{id}/subject` — the node-config subject substrate gate
|
|
3
4
|
* predicates evaluate against. Mirrors `NodeConfigSubject`; this narrow
|
|
4
5
|
* endpoint exists so a CLI process (the `memory read` leaf) can gate-check
|
|
@@ -79,8 +80,19 @@ export interface NodeSummaryDTO {
|
|
|
79
80
|
window: string | null;
|
|
80
81
|
tmux_session: string | null;
|
|
81
82
|
pane: string | null;
|
|
83
|
+
/** Basename of the canonical final report, or null when this node has not finalized. */
|
|
82
84
|
final_report: string | null;
|
|
83
85
|
finalized_at: IsoTime | null;
|
|
86
|
+
/** Present only when requested with `include=activity`. */
|
|
87
|
+
activity?: NodeActivityDTO;
|
|
88
|
+
}
|
|
89
|
+
/** Optional per-node activity payload for a list response. Reports are the
|
|
90
|
+
* stored push entries, newest first; `final_report` follows the row's canonical
|
|
91
|
+
* final-report basename rather than merely selecting the newest final-tier file. */
|
|
92
|
+
export interface NodeActivityDTO {
|
|
93
|
+
latest_report: ReportDTO | null;
|
|
94
|
+
final_report: ReportDTO | null;
|
|
95
|
+
pending_human_count: number;
|
|
84
96
|
}
|
|
85
97
|
/** The spine + subscription edges of a node (absorbs `managers`/`paths` reads). */
|
|
86
98
|
export interface NodeEdgesDTO {
|
|
@@ -166,15 +178,29 @@ export interface NodeDetailDTO extends NodeSummaryDTO {
|
|
|
166
178
|
roadmap_path?: string;
|
|
167
179
|
goal_path?: string;
|
|
168
180
|
}
|
|
169
|
-
/** `GET /v1/nodes`
|
|
181
|
+
/** `GET /v1/nodes` optional payloads. */
|
|
182
|
+
export type NodeListInclude = 'activity';
|
|
183
|
+
/** `GET /v1/nodes` query filters. Filters compose; `parent` selects direct
|
|
184
|
+
* children while `under` selects a whole subtree. */
|
|
170
185
|
export interface ListNodesQuery {
|
|
171
186
|
status?: NodeStatusDTO;
|
|
187
|
+
lifecycle?: LifecycleDTO;
|
|
172
188
|
kind?: string;
|
|
173
189
|
mode?: ModeDTO;
|
|
190
|
+
/** Exact profile id. */
|
|
191
|
+
profile_id?: string;
|
|
192
|
+
/** Profile-id prefix, for callers that own a namespaced profile family. */
|
|
193
|
+
profile_prefix?: string;
|
|
194
|
+
/** Restrict to direct children of this node. Mutually exclusive with `top_level`. */
|
|
195
|
+
parent?: NodeIdDTO;
|
|
196
|
+
/** Restrict to nodes without a parent. */
|
|
197
|
+
top_level?: boolean;
|
|
174
198
|
/** Restrict to the subtree under this node. */
|
|
175
199
|
under?: NodeIdDTO;
|
|
176
200
|
/** Only nodes with a dangling/hanging manager edge. */
|
|
177
201
|
hanging?: boolean;
|
|
202
|
+
/** Opt into richer per-node payloads that require filesystem reads. */
|
|
203
|
+
include?: NodeListInclude;
|
|
178
204
|
}
|
|
179
205
|
/** `GET /v1/nodes/{id}/snapshot` — the node's reconstructed broker snapshot
|
|
180
206
|
* (`readNodeSnapshot`): the message log, aggregate stats, and current engine
|
|
@@ -4,6 +4,8 @@ when-and-why-to-read: When a node runs in base mode, this preference should be r
|
|
|
4
4
|
gate: {mode: base}
|
|
5
5
|
rationale: >-
|
|
6
6
|
A base security reviewer handed its entire assignment to another base security reviewer, which repeated the move through a 35-node chain in under five minutes. The universal prompt had said to delegate any self-contained work while no base-mode layer told sub-kinds to work hands-on; exact sub-kind gating also meant plan/reviewers/security did not inherit plan/00-base.
|
|
7
|
+
|
|
8
|
+
The scout section exists because the kernel's "understand before you delegate" line is orchestrator-gated, so base workers had no counterweight to mapping unfamiliar code in their own window: they spent a strong-model context on read-only exploration and yielded before the real work. Explore defaults to a light tier, which is the mechanism the section leans on.
|
|
7
9
|
surfaces:
|
|
8
10
|
- on: boot
|
|
9
11
|
at: content
|
|
@@ -13,4 +15,8 @@ surfaces:
|
|
|
13
15
|
|
|
14
16
|
Own the task you were given and complete it yourself in this window. Your scarce resource is task completion, not preserving your context for steering.
|
|
15
17
|
|
|
16
|
-
Delegate only a genuinely independent subtask that can run in parallel while you continue owning the parent task. Each child gets a bounded outcome distinct from your whole assignment; passing the same task to another base node creates recursion instead of progress.
|
|
18
|
+
Delegate only a genuinely independent subtask that can run in parallel while you continue owning the parent task. Each child gets a bounded outcome distinct from your whole assignment; passing the same task to another base node creates recursion instead of progress.
|
|
19
|
+
|
|
20
|
+
One delegation fits that rule before the work even starts: when the task sits in code you cannot yet map — you don't know which files it touches or which constraints hold — spawn an `explore` scout to chart it. A current-state map is a bounded outcome distinct from your assignment, and explore runs on a light model tier, so the `file:line` map it wakes you with spends the scout's cheap window instead of the context you need for the work itself. Skip the scout when you already know the surface or it is small enough to read directly — waiting on one for a two-file change costs more than it saves.
|
|
21
|
+
|
|
22
|
+
When the goal will not fit one window but is still yours to build, `crtr node yield` and continue hands-on in a fresh window. Become an orchestrator only when enough independent work can run in parallel that coordinating and integrating children should replace hands-on execution as your primary job; use `crtr node yield --promote` to refresh too, or `crtr node promote` to switch without refreshing.
|
|
@@ -69,6 +69,73 @@ The `<plugin-name>` directory IS the plugin. The manifest's `name` field must ma
|
|
|
69
69
|
| `bin` | optional | Bare-callable executables the plugin ships, as `{"<bare-name>": "<plugin-relative path>"}`. Each becomes callable by bare name from any node's bash — no `crtr` command, no protocol. See [Bare binaries on the node PATH](#bare-binaries-on-the-node-path). |
|
|
70
70
|
| `requires` | optional | Advisory external executables the plugin needs, as `{"<bare-name>": "one-line install hint"}`. crouter never installs them or disables the plugin when absent: install warns and Doctor reports the hint. See [External executable requirements](#external-executable-requirements). |
|
|
71
71
|
| `kinds` | optional | Kind-registry contributions, keyed by full kind string — the same rule a scope `config.json` `kinds` block follows: each entry is any subset of `KindConfig` fields (`whenToUse`, `model`, `orchestratorModel`, `tools`, `extensions`, `availableTo`). It field-merges over a kind a lower layer already defines — `whenToUse` included, so overriding just the spawn guidance never strips the kind's model tier — and defines a new kind when none does (then `whenToUse` is required). See [Plugin kinds](#plugin-kinds). |
|
|
72
|
+
| `memory_extensions` | optional | Typed plugin-owned metadata declarations for memory document frontmatter. The manifest name owns the namespace; see [Plugin-owned memory metadata](#plugin-owned-memory-metadata). |
|
|
73
|
+
|
|
74
|
+
## Plugin-owned memory metadata
|
|
75
|
+
|
|
76
|
+
A plugin can declare typed metadata that crouter validates and carries without teaching the core memory schema what the metadata means. This keeps routing, loading, listings, and agent delivery governed only by core frontmatter while a plugin's own product surfaces interpret its fields.
|
|
77
|
+
|
|
78
|
+
Declare fields in the optional `memory_extensions` manifest addendum. The manifest's exact `name`, not a configurable declaration value or installation path, is always the namespace:
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"name": "northlight",
|
|
83
|
+
"memory_extensions": {
|
|
84
|
+
"visibility": {
|
|
85
|
+
"type": "boolean",
|
|
86
|
+
"default": true,
|
|
87
|
+
"write_help": "Control whether this memory appears in Northlight's human memory surfaces.",
|
|
88
|
+
"edit_help": "Change whether this memory appears in Northlight's human memory surfaces."
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Each addendum is a nonempty object keyed by lowercase kebab-case local field names. A declaration has no keys beyond these:
|
|
95
|
+
|
|
96
|
+
| Key | Required | Contract |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| `type` | yes | `boolean`, `string`, `number`, or `enum`. |
|
|
99
|
+
| `values` | enum only | A nonempty, unique list of nonempty strings. It is invalid for every other type. |
|
|
100
|
+
| `default` | no | A value matching `type`; an enum default must be one of `values`. |
|
|
101
|
+
| `write_help` | yes | Nonblank single-line text generated into the field catalog on `crtr memory write -h`. |
|
|
102
|
+
| `edit_help` | yes | Nonblank single-line text generated into the field catalog on `crtr memory edit -h`. |
|
|
103
|
+
|
|
104
|
+
The generated catalogs show each installed field's full path, type (or enum alternatives), and default when declared. They are the maintained authoring surface; the addendum is their sole help source. Read those leaves for the set, change, and unset mechanics.
|
|
105
|
+
|
|
106
|
+
Document values live directly in frontmatter under `extensions.<plugin-name>.<field>` — never beneath a `properties` layer:
|
|
107
|
+
|
|
108
|
+
```yaml
|
|
109
|
+
extensions:
|
|
110
|
+
northlight:
|
|
111
|
+
visibility: false
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`extensions` and each namespace must be mappings. Values may only be strings, booleans, or finite numbers; a declared field then must satisfy its own type, and an enum must be one of its declared strings. A plugin can declare only fields in its own manifest-name namespace. Two plugins can use the same local field name because their namespaces remain distinct.
|
|
115
|
+
|
|
116
|
+
A default is interpretation, not persistence: an absent declared field resolves to its default only in the effective metadata projection and is never written back into frontmatter. `crtr memory read --frontmatter` exposes exactly the explicit raw mapping. Structured `crtr --json memory read` and `crtr --json memory list` expose only effective, valid metadata in `extensions`, overlaying explicit values on enabled declarations' defaults. Ordinary read and list rendering deliberately omit the metadata, and crouter never injects it into a memory body or agent-facing memory render.
|
|
117
|
+
|
|
118
|
+
Crouter maintains separate declaration views for validation and interpretation. Validation and generated authoring help use the closest installed declaration even when its plugin is disabled, so an existing value remains type-checked and can be changed or removed. Effective structured metadata uses only the winning enabled declaration. Therefore disabling a plugin leaves its raw frontmatter in place but omits that namespace and its defaults from effective output; it has no crouter behavior of its own. If the owner is removed or cannot resolve, the namespace is raw-only and inert, lint reports it as unresolved, and unrelated core-frontmatter edits preserve it. Re-enabling or reinstalling the owner restores explicit values and defaults to effective interpretation without rewriting the document.
|
|
119
|
+
|
|
120
|
+
Archive plugins declare the identical addendum in `bundle.json` beside `bundleVersion`:
|
|
121
|
+
|
|
122
|
+
```json
|
|
123
|
+
{
|
|
124
|
+
"bundleVersion": 1,
|
|
125
|
+
"memory_extensions": {
|
|
126
|
+
"visibility": {
|
|
127
|
+
"type": "boolean",
|
|
128
|
+
"default": true,
|
|
129
|
+
"write_help": "Control whether this memory appears in a human-facing view.",
|
|
130
|
+
"edit_help": "Change whether this memory appears in a human-facing view."
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The archive validator applies the same closed declaration contract and copies the accepted block into the synthesized installed `plugin.json`; archive and source plugins therefore present one installed declaration model.
|
|
137
|
+
|
|
138
|
+
`crtr memory lint` validates extension mappings and reports malformed mappings, unresolved namespaces, undeclared fields, non-scalar values, wrong types, and invalid enum values at their field paths. Installation and every update validate the candidate declaration plus every candidate `memory/**/*.md` document against the shared core-frontmatter and extension contracts before activation. The candidate's own declaration governs its package while it is checked, including when it changes or removes a declaration present in the installed version. Candidates are staged outside active plugin paths and replace the active package only after validation; a failed source, archive, or marketplace candidate never becomes reachable through an active plugin path, and the previous package and recorded version remain active.
|
|
72
139
|
|
|
73
140
|
## Plugin kinds
|
|
74
141
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: knowledge
|
|
3
|
+
when-and-why-to-read: When an agent reads its first memory document, this knowledge should be read because the inspection and revision affordances stay visible without taxing every later read.
|
|
4
|
+
rationale: The generic follow-up appeared after every memory read even though transcript-scoped delivery can teach it once; Silas asked to move it into a one-time built-in memory on 2026-08-17.
|
|
5
|
+
unlisted: true
|
|
6
|
+
lint-ignore: broad-memory-read
|
|
7
|
+
surfaces:
|
|
8
|
+
- on: memory-read
|
|
9
|
+
match: "**"
|
|
10
|
+
at: content
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
`crtr memory read --frontmatter <name>` includes YAML frontmatter. Revise a document with `crtr memory edit` (recorded, with a rationale), or browse the inventory with `crtr memory list`.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// activeBackgroundBashJobs' job, in core/bash-jobs.ts). Enter on that row opens
|
|
5
5
|
// the Inspector's `jobs` section, which owns the drill-in rendering.
|
|
6
6
|
import { bashJobCommandLine, formatBashElapsed } from '../../../core/bash-jobs.js';
|
|
7
|
-
/** The job's
|
|
7
|
+
/** The job's command line, capped to `cap` columns. A
|
|
8
8
|
* shell one-liner is cut on a word boundary when there is one near the cap, so
|
|
9
9
|
* the cell ends on a readable token instead of mid-`$((`. */
|
|
10
10
|
function cmdCell(command, cap) {
|
|
Binary file
|