@rayul/kordis 0.15.2 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/MEMGINEERING.md +1 -1
- package/assets/kordis-publishing/SKILL.md +108 -45
- package/assets/kordis-social/SKILL.md +19 -0
- package/assets/kordis-using/SKILL.md +7 -1
- package/assets/memgineering-recall/SKILL.md +64 -10
- package/assets/memgineering-setup/SKILL.md +15 -9
- package/dist/index.js +366 -107
- package/package.json +1 -1
package/assets/MEMGINEERING.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: memgineering
|
|
3
3
|
description: Use whenever the user refers to something they told you before, asks what was decided, tells you something worth keeping, or settles something that should hold next time. The memory lives in their own folder and outlives this session; check it before answering from guesswork, and write to it when you learn something durable.
|
|
4
4
|
type: skill
|
|
5
|
-
version: 0.
|
|
5
|
+
version: 0.27.0
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# memgineering
|
|
@@ -20,19 +20,46 @@ Publish a valid, AI-review-passing version. ALWAYS dry-run first.
|
|
|
20
20
|
- `license` — SPDX (e.g. `MIT`, `Apache-2.0`, `CC-BY-4.0`).
|
|
21
21
|
- Optional: `homepage`, `tags`, `type`.
|
|
22
22
|
|
|
23
|
-
**`type`
|
|
24
|
-
|
|
25
|
-
`
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
**`type` and `--type`:** A package is published as the type its
|
|
24
|
+
frontmatter's `type:` says, and nothing else. When the frontmatter has
|
|
25
|
+
no `type:` line, the CLI writes one in: `type: skill` for a directory
|
|
26
|
+
bundle, a `.skill` ZIP, or a single file named `SKILL.md` or
|
|
27
|
+
`*.skill.md` — so none of these needs `type:` written — or `type: <t>`
|
|
28
|
+
when you pass `--type <t>`, on any path. When the frontmatter names a
|
|
29
|
+
type, `--type` must say the same: a different value is refused before
|
|
30
|
+
anything is sent (`usage_error`, exit 2, in `--dry-run` too). Change the
|
|
31
|
+
frontmatter's `type:`, or drop `--type`.
|
|
32
|
+
|
|
33
|
+
Nothing is written over a `type:` line that is already there, or into a
|
|
34
|
+
file that begins with a byte-order mark. These fail instead, in
|
|
35
|
+
`--dry-run` too; write `type: <t>` in by hand:
|
|
36
|
+
|
|
37
|
+
- `type:` with no value (or `null`, a number, a list), and a file with a
|
|
38
|
+
byte-order mark and no `type:` line: `missing_required_frontmatter`
|
|
39
|
+
(exit 30), with or without `--type`.
|
|
40
|
+
- `type: ""`, an empty string: without `--type` it fails as
|
|
41
|
+
`unknown_type` (exit 30); with `--type` it disagrees, so it is refused
|
|
42
|
+
before anything is sent (`usage_error`, exit 2).
|
|
43
|
+
|
|
44
|
+
**Older CLIs (0.15.2 and earlier)** send `--type` only beside the
|
|
45
|
+
content and never write it in. They fill in `type: skill` themselves
|
|
46
|
+
when the frontmatter names no type: for a folder or `.skill` bundle
|
|
47
|
+
from 0.1.12, and for a single `SKILL.md` or `*.skill.md` from 0.6.4.
|
|
48
|
+
The registry refuses a `--type` that differs from the type it reads —
|
|
49
|
+
written or filled in — with `usage_error` (exit 2) and a hint naming
|
|
50
|
+
that type. From 0.2.2 their `--dry-run` checks the file locally and
|
|
51
|
+
sends no publish request (from 0.9.1, `--bump minor` or `major` makes
|
|
52
|
+
it look the package up), so it does not show this refusal; only a real
|
|
53
|
+
push does. 0.2.1 and earlier send the dry run to the registry, which
|
|
54
|
+
refuses it the same way as a real push. Writing `type:` in the
|
|
55
|
+
frontmatter works on every version.
|
|
29
56
|
|
|
30
57
|
**`version` field handling:**
|
|
31
58
|
|
|
32
59
|
- **First publish** — used as-is (else defaults to `0.1.0`).
|
|
33
60
|
- **Update** — the registry computes the next version from
|
|
34
61
|
`current_version + --bump <strategy>` (default `patch`). If the file's
|
|
35
|
-
`version` differs from that result, the API rejects with `
|
|
62
|
+
`version` differs from that result, the API rejects with `usage_error`. Either
|
|
36
63
|
drop the `version` field on updates, set `--bump` to match, or align
|
|
37
64
|
them.
|
|
38
65
|
- RECOMMENDED: **omit `version:` from frontmatter on update pushes** and
|
|
@@ -70,8 +97,11 @@ get rejected. Dry-run first like any push.
|
|
|
70
97
|
## Recording lineage (0.12.0+)
|
|
71
98
|
|
|
72
99
|
When a package descends from another (especially a judgment you adapted), record
|
|
73
|
-
the provenance on `push
|
|
74
|
-
|
|
100
|
+
the provenance on `push`. It shows up in the "Derived from / Adapted by" graph,
|
|
101
|
+
and when the derivative is another author's — public, with its current version
|
|
102
|
+
passed review — it also feeds the source's `community-adapted` trust tier and
|
|
103
|
+
gives the source a small trending boost. A derivative of a package you authored
|
|
104
|
+
yourself does neither. Record it with one of these flags:
|
|
75
105
|
|
|
76
106
|
| flag | edge kind | meaning |
|
|
77
107
|
| ----------------------- | ------------ | ----------------------------------------------- |
|
|
@@ -82,10 +112,10 @@ feeds the `community-adapted` trust tier, and gives a small trending boost:
|
|
|
82
112
|
|
|
83
113
|
A source that does not resolve — a typo'd slug, or a package that is
|
|
84
114
|
unpublished, deleted or private — **refuses the whole publish** with
|
|
85
|
-
`usage_error` (exit 2), and `--dry-run`
|
|
86
|
-
|
|
87
|
-
first, then push again. (Naming your own package as its source is
|
|
88
|
-
instead, and listed in the response's `lineage.skipped`.) Declare
|
|
115
|
+
`usage_error` (exit 2), and nothing is written. `--dry-run` does not look
|
|
116
|
+
sources up, so a typo first shows on the real push. Fix the slug, or publish
|
|
117
|
+
the source first, then push again. (Naming your own package as its source is
|
|
118
|
+
skipped instead, and listed in the response's `lineage.skipped`.) Declare
|
|
89
119
|
lineage honestly; it is the registry's trust signal, not decoration.
|
|
90
120
|
|
|
91
121
|
## Yanking a bad version (0.10.0+)
|
|
@@ -163,7 +193,7 @@ operator restores it.
|
|
|
163
193
|
| `--dry-run` | validate only; do not publish |
|
|
164
194
|
| `--bump <strategy>` | `patch` (default) / `minor` / `major` — only on update |
|
|
165
195
|
| `--slug <slug>` | override slug on first publish |
|
|
166
|
-
| `--type <type>` |
|
|
196
|
+
| `--type <type>` | fills a missing `type:`; must match an existing one |
|
|
167
197
|
| `--changelog <text>` | optional changelog entry attached to this version |
|
|
168
198
|
| `--no-wait` | return immediately without waiting for AI review |
|
|
169
199
|
| `--adapted-from <slug>` / `--forked-from` / `--absorbs` / `--remixes` | record lineage (see "Recording lineage") |
|
|
@@ -174,29 +204,58 @@ operator restores it.
|
|
|
174
204
|
|
|
175
205
|
### 1. Dry-run
|
|
176
206
|
|
|
177
|
-
`kordis push <file> --dry-run --json
|
|
178
|
-
and
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
207
|
+
`kordis push <file> --dry-run --json` runs on this machine (0.2.2+): it
|
|
208
|
+
sends no publish request and needs no sign-in. It validates the content
|
|
209
|
+
the real push would send — the frontmatter, including any `type:` the CLI
|
|
210
|
+
writes in, the body, and bundled files' paths and sizes — and answers
|
|
211
|
+
`{status, type, tier, capabilities, asset_count, flags, warnings, note}`
|
|
212
|
+
with `status` `dry_run_passed` (exit 0) or `dry_run_failed` (exit 30).
|
|
213
|
+
A pass can still list `flags` that do not block. `warnings` come from a
|
|
214
|
+
local scan for content that looks private — `ipv4_or_internal_host`,
|
|
215
|
+
`credential_like_string`, `absolute_home_path`,
|
|
216
|
+
`first_person_context_hint` — and never block.
|
|
217
|
+
|
|
218
|
+
It decides nothing the registry decides: no name conflict, no version (a
|
|
219
|
+
frontmatter `version` is not checked against the bump), no lineage
|
|
220
|
+
source, no declared-capability check, no rate limit, and no AI review.
|
|
221
|
+
Apart from finishing a pending `kordis login --emit-only`, as every
|
|
222
|
+
command does, its one registry call is a package lookup for
|
|
223
|
+
`--bump minor` or `--bump major` (0.9.1+), made when it can tell which
|
|
224
|
+
package: the frontmatter has a `name`, and you are signed in or `--slug`
|
|
225
|
+
names `<user>/<slug>`. A bump on a package that does not exist yet is
|
|
226
|
+
refused — `usage_error`, exit 2, from 0.9.2 (0.9.1 exits 1) — and if the
|
|
227
|
+
lookup fails any other way, the check is skipped. Only a real push gets
|
|
228
|
+
the registry's verdicts, including the AI review, which can land
|
|
229
|
+
`passed` / `rejected`, or leave the publish `status: "pending"` if the
|
|
230
|
+
worker hasn't decided within the wait window.
|
|
231
|
+
|
|
232
|
+
**Success criteria**: `status: "dry_run_passed"`.
|
|
233
|
+
|
|
234
|
+
### 2. Resolve what comes back
|
|
235
|
+
|
|
236
|
+
From the dry run:
|
|
237
|
+
|
|
238
|
+
- `dry_run_failed` (exit 30) — fix every listed flag, dry-run again.
|
|
239
|
+
- `usage_error` (exit 2) — a `--type` the frontmatter contradicts, or
|
|
240
|
+
`--bump minor` / `major` on a first publish; the message says which.
|
|
241
|
+
- `generic_failure` (exit 1) with a YAML hint — the frontmatter does not
|
|
242
|
+
parse; fix it as the hint says.
|
|
243
|
+
- `generic_failure` (exit 1) — the path is not a package the CLI can read:
|
|
244
|
+
it does not exist, a folder has no SKILL.md at its top, or a `.skill`
|
|
245
|
+
archive is broken or has no SKILL.md; the message says which.
|
|
246
|
+
|
|
247
|
+
Only from the real push (step 3), however clean the dry run was:
|
|
248
|
+
|
|
249
|
+
- `validation_failed` (exit 30) — the registry's checks: the dry run's,
|
|
250
|
+
plus ones it does not make, such as a `hook` or `mcp-server` that
|
|
251
|
+
declares `executable: false`; fix every listed flag.
|
|
252
|
+
- `usage_error` (exit 2) — for example a lineage source that does not
|
|
253
|
+
resolve, or a frontmatter `version` the bump does not produce; the
|
|
254
|
+
message says which. Nothing is written.
|
|
195
255
|
- `conflict` (exit 40) — slug taken; suggest a different slug (or pass
|
|
196
256
|
`--slug` if first publish).
|
|
197
|
-
- Review rejection —
|
|
198
|
-
|
|
199
|
-
and returns `status: "rejected"` with the reasons in `review.flags`
|
|
257
|
+
- Review rejection — not an error: the push exits 0 and returns
|
|
258
|
+
`status: "rejected"` with the reasons in `review.flags`
|
|
200
259
|
(`ai_review_rejected`, exit 32, is defined but never sent). EXPLAIN the
|
|
201
260
|
rejection to the user. Do NOT silently retry. Includes deterministic
|
|
202
261
|
Stage-1 hits like `curl_pipe_shell`, `aws_access_key`,
|
|
@@ -204,7 +263,7 @@ Possible verdicts:
|
|
|
204
263
|
- `rate_limited` (exit 31) — wait `retry_after_seconds`.
|
|
205
264
|
- `not_authenticated` (exit 10) — `kordis login`, then retry.
|
|
206
265
|
|
|
207
|
-
**Success criteria**: dry
|
|
266
|
+
**Success criteria**: the dry run passed.
|
|
208
267
|
|
|
209
268
|
### 3. Real push
|
|
210
269
|
|
|
@@ -219,9 +278,10 @@ yet — re-check with `kordis info <full_slug> --json` (or wait and re-run
|
|
|
219
278
|
## Versioning your push (0.9.0+)
|
|
220
279
|
|
|
221
280
|
- **First publish** derives the version from the frontmatter `version:`
|
|
222
|
-
field if present, else defaults to `0.1.0`. Passing `--bump`
|
|
223
|
-
first publish now returns a clear `usage_error`
|
|
224
|
-
ignored it)
|
|
281
|
+
field if present, else defaults to `0.1.0`. Passing `--bump minor` or
|
|
282
|
+
`--bump major` on a first publish now returns a clear `usage_error`
|
|
283
|
+
(pre-0.9 silently ignored it); `--bump patch`, the default, is ignored
|
|
284
|
+
there — drop `--bump` or set the frontmatter version explicitly.
|
|
225
285
|
- **Subsequent pushes** default to `--bump patch`. Explicit
|
|
226
286
|
`--bump minor` or `--bump major` ships the matching jump.
|
|
227
287
|
- **Frontmatter `version:` on an update** must match the result of
|
|
@@ -249,19 +309,21 @@ silently retry — explain why.
|
|
|
249
309
|
User (push-back after adapting a judgment): "that decision rule we adapted from
|
|
250
310
|
<upstream/slug> worked well — share our version back"
|
|
251
311
|
1. author the refitted judgment as a judgment-pattern file (structured
|
|
252
|
-
frontmatter
|
|
312
|
+
frontmatter with `type: judgment-pattern`, which the `--type` below
|
|
313
|
+
must match; see "Publishing a judgment-pattern")
|
|
253
314
|
2. kordis push ./judgment.md --type judgment-pattern --adapted-from <upstream/slug> --dry-run --json
|
|
254
315
|
3. clean → drop --dry-run for the real push
|
|
255
316
|
4. the response's `lineage.recorded` confirms the edge. A wrong or unpublished
|
|
256
|
-
upstream slug
|
|
257
|
-
|
|
258
|
-
|
|
317
|
+
upstream slug is refused here, by the real push, with `usage_error` and
|
|
318
|
+
nothing written — the dry run in step 2 does not look sources up. Fix the
|
|
319
|
+
slug, or publish the source first. The edge gives the upstream
|
|
320
|
+
`community-adapted` credit and shows in its "Adapted by" graph.
|
|
259
321
|
</example>
|
|
260
322
|
|
|
261
323
|
<example>
|
|
262
324
|
User: "publish my new skill"
|
|
263
325
|
1. kordis push ./SKILL.md --dry-run --json
|
|
264
|
-
2.
|
|
326
|
+
2. dry_run_failed → list each flag, fix the file, re-run
|
|
265
327
|
3. clean → kordis push ./SKILL.md --json
|
|
266
328
|
4. show {full_slug, version, url}
|
|
267
329
|
</example>
|
|
@@ -271,7 +333,8 @@ User: "release v0.2.0 of <full_slug>"
|
|
|
271
333
|
- previous version was 0.1.x
|
|
272
334
|
1. drop or align the frontmatter `version` field — `--bump minor` from
|
|
273
335
|
0.1.x will yield 0.2.0; if the file says `0.2.0` that matches and is OK,
|
|
274
|
-
if it says anything else (e.g. `0.1.0`) the
|
|
336
|
+
if it says anything else (e.g. `0.1.0`) the real push is refused (the
|
|
337
|
+
dry run does not compare versions)
|
|
275
338
|
2. kordis push <file> --bump minor --dry-run --json
|
|
276
339
|
3. clean → kordis push <file> --bump minor --json
|
|
277
340
|
</example>
|
|
@@ -51,6 +51,23 @@ Post a top-level comment on a package. Auth required.
|
|
|
51
51
|
- `question` — explicit question expecting a reply (typically from the
|
|
52
52
|
author). Use when the user wants a response, not just to share thought.
|
|
53
53
|
|
|
54
|
+
**What the server refuses.** A comment's title and body get the same content
|
|
55
|
+
screen a published package gets: a download piped into a shell, an encoded
|
|
56
|
+
`eval`, API keys, tokens and private keys, and acceptable-use violations
|
|
57
|
+
(markets for stolen data, malware or attacks for hire, drug and weapon
|
|
58
|
+
brokering, abuse material, targeted harassment). A hit refuses the whole
|
|
59
|
+
comment with `usage_error` (exit 2) and nothing is posted, so there is nothing
|
|
60
|
+
to delete. The message names the field that matched (`comment title rejected …`
|
|
61
|
+
or `comment body rejected …`). Rewrite that field as the hint says and run the
|
|
62
|
+
command again; never resend the same text.
|
|
63
|
+
|
|
64
|
+
- **Reports: describe, do not quote.** A `report` that pastes the offending
|
|
65
|
+
line of a dangerous package (a `curl … | bash`, a key) is refused by the
|
|
66
|
+
same screen. Say what the step does and where it is instead.
|
|
67
|
+
- **Marker lines come back `[redacted]`.** A title or body line that imitates
|
|
68
|
+
the registry's untrusted-content markers is stored as `[redacted]`. Show the
|
|
69
|
+
user the `title` the response returns, not the one you sent.
|
|
70
|
+
|
|
54
71
|
Replies, edits, status flips (open/resolved on a `report`), listing
|
|
55
72
|
existing comments, and reading back your own comments are NOT yet exposed
|
|
56
73
|
in the CLI. Direct the user to `https://kordis.store/<full_slug>` for
|
|
@@ -140,6 +157,8 @@ User: "leave a tip on foo/bar saying it pairs well with baz/qux"
|
|
|
140
157
|
User: "report that the v0.3.0 release of foo/bar is broken on macOS"
|
|
141
158
|
- kordis comment foo/bar --kind report --title "v0.3.0 broken on macOS" \
|
|
142
159
|
--body "<reproducible details>" --json
|
|
160
|
+
- if it is refused (`usage_error`, exit 2), describe the problem without
|
|
161
|
+
quoting the offending text and post again
|
|
143
162
|
- mention that opening/resolving the report later requires the web UI
|
|
144
163
|
</example>
|
|
145
164
|
|
|
@@ -56,7 +56,13 @@ Returns the full body (Markdown) of a text-bearing package, plus an
|
|
|
56
56
|
`attribution` string. `--pin <semver>` overrides any `@suffix` on the slug.
|
|
57
57
|
**Context economy**: `--outline` returns just the section map (heading names);
|
|
58
58
|
then `--section <name>` returns one section's body. Read the outline first
|
|
59
|
-
when a body is large and you only need part of it.
|
|
59
|
+
when a body is large and you only need part of it. A section name the package
|
|
60
|
+
does not have exits 2 (`usage_error`): under `--json` the refusal is the error
|
|
61
|
+
envelope on stdout, and its `hint` lists the section names that do exist (or,
|
|
62
|
+
for a package with none, says to read the whole body); without `--json` it goes
|
|
63
|
+
to stderr only, so a body redirected into a file never captures it. Either way,
|
|
64
|
+
the `kordis read` command it prints keeps the version, `--pin` and
|
|
65
|
+
`--allow-pending` you read with, so it reaches the same body.
|
|
60
66
|
|
|
61
67
|
**Untrusted-content fence (all output modes, incl. `--json`)**: the body (and
|
|
62
68
|
outline description / section slice) arrives wrapped between exactly two lines
|
|
@@ -191,18 +191,72 @@ summary before you answer from it — never quote it back on the label alone.
|
|
|
191
191
|
## Two answers that are answers
|
|
192
192
|
|
|
193
193
|
**Nothing matched.** Say so, and do not go read their folder yourself to
|
|
194
|
-
compensate. At the default depth this has already searched
|
|
195
|
-
**the brain it searched**, so "nothing" is real
|
|
196
|
-
rather than a hint to search harder — read
|
|
197
|
-
words are still worth one retry;
|
|
198
|
-
less.
|
|
194
|
+
compensate. At the default depth this has already searched the body of every
|
|
195
|
+
note recall could read in **the brain it searched**, so "nothing" is real
|
|
196
|
+
information about those notes rather than a hint to search harder — read
|
|
197
|
+
`semantic` first, per above. Broader words are still worth one retry;
|
|
198
|
+
`--depth exact` is not — it looks at strictly less.
|
|
199
|
+
|
|
200
|
+
In a brain in a folder, that is not a promise about every file. Notes it left
|
|
201
|
+
out are counted below, but recall never looks inside a folder the user's own
|
|
202
|
+
rules exclude (`.gitignore`, `.memgignore`, the rules file): notes in one are
|
|
203
|
+
neither searched nor counted, and an answer where other notes were searched does
|
|
204
|
+
not mention them. So an empty answer is not proof that the user never wrote
|
|
205
|
+
something down. Say what was searched, and that folders their rules exclude
|
|
206
|
+
were not, rather than telling them they never wrote it. Do not open such a
|
|
207
|
+
folder yourself.
|
|
199
208
|
|
|
200
209
|
One brain is not the account. On a hosted account with more than one brain, an
|
|
201
|
-
empty answer
|
|
202
|
-
`--brain` command for each. Run it before you tell
|
|
203
|
-
written down — four testers reached that conclusion
|
|
204
|
-
next brain, and three of them said it out loud.
|
|
205
|
-
move the machine's pointer.
|
|
210
|
+
empty answer — or one of template cards only — names the brains that were NOT
|
|
211
|
+
read and prints a runnable `--brain` command for each. Run it before you tell
|
|
212
|
+
them the fact was never written down — four testers reached that conclusion
|
|
213
|
+
about a note sitting in the next brain, and three of them said it out loud.
|
|
214
|
+
Reading another brain does not move the machine's pointer.
|
|
215
|
+
|
|
216
|
+
With `--json`, an answer with nothing the user wrote in it carries `hint`: no
|
|
217
|
+
cards, or only template cards (`scaffolding: true`, below) — those are what
|
|
218
|
+
`init` wrote, not the user's notes. `hint` is the one next step, the most
|
|
219
|
+
specific that applies. First, the brain had nothing to search: it has no notes
|
|
220
|
+
yet (the `remember` command that writes the first one into that brain — or,
|
|
221
|
+
after `--brain` named a brain this machine does not point at, the
|
|
222
|
+
`memgineering use --brain "<name>"` that makes it the target, since `remember`
|
|
223
|
+
has no `--brain`), or it is a folder whose every note was left out (how many,
|
|
224
|
+
why, and the `memgineering reindex` command that names each one). Then: its
|
|
225
|
+
notes are in a script the question is not (ask again in that language), another
|
|
226
|
+
brain was not read (the `--brain` or `--vault` command that reads it), meaning
|
|
227
|
+
was not searched (and the server's reason), the hosted server's own advice, or
|
|
228
|
+
fewer and broader words. Read it before you conclude anything from
|
|
229
|
+
`cards: []` or from template cards. A `memgineering use --brain` in a hint
|
|
230
|
+
moves this machine's pointer for every later command, not just the next one:
|
|
231
|
+
offer it to the user and run it only on their yes. `hint` is absent whenever a
|
|
232
|
+
card they wrote came back.
|
|
233
|
+
|
|
234
|
+
**Held-back notes are counted in both modes.** When an answer has nothing the
|
|
235
|
+
user wrote in it, recall counts what a brain in a folder left out, says why,
|
|
236
|
+
and prints the `memgineering reindex` command that names each one: in the
|
|
237
|
+
terminal right under `_Nothing matched._` or under the template cards' header,
|
|
238
|
+
and at the front of `hint`. It opens with "Nothing in this brain could be
|
|
239
|
+
searched: …" when no note could be searched at all, and with "Not every note in
|
|
240
|
+
this brain was searched: …" when other notes were searched and at least one
|
|
241
|
+
note of the first kind below was left out. Recall itself never names or shows a
|
|
242
|
+
left-out note. The counts come in two kinds, and they mean different things:
|
|
243
|
+
|
|
244
|
+
- **Notes**: excluded by the user's rules (`.gitignore`, `.memgignore`, the
|
|
245
|
+
rules file), listed in `.memgdeny`, held back because the name looks private,
|
|
246
|
+
refused because the content looks like a credential, or behind a symlink. A
|
|
247
|
+
note the user wrote exists and was left out on purpose. Do NOT tell them the
|
|
248
|
+
brain is empty or that they never wrote it down: tell them such a note exists,
|
|
249
|
+
and why; to say where it is, run that `reindex` — it lists each path and
|
|
250
|
+
changes no note — and give them the path. Do not open, print or read a
|
|
251
|
+
held-back note into the conversation unless they ask what it says: it was
|
|
252
|
+
held back because it may be private.
|
|
253
|
+
- **Places nobody looked inside**: an excluded folder ("not looked inside"), or
|
|
254
|
+
another symlink, which is never followed. These are not evidence of a note —
|
|
255
|
+
an ignored `images/` or `.venv/` holds none. Say that recall could not look
|
|
256
|
+
inside them and that `reindex` names them; do not say a note was held back.
|
|
257
|
+
When "Nothing in this brain could be searched" lists only these, the brain may
|
|
258
|
+
hold no notes at all. Beside notes that were searched they are not counted at
|
|
259
|
+
all — see **Nothing matched** above.
|
|
206
260
|
|
|
207
261
|
**Cards that are not answers.** A card marked `not filled in yet` is one of the
|
|
208
262
|
five `01_BASE/` files, still exactly what `init` wrote (line endings aside). Nobody has answered
|
|
@@ -506,17 +506,23 @@ by hand in the ordinary case — writes keep the index in step on their own.
|
|
|
506
506
|
what is missing and clears stored rows for notes the brain has since been told
|
|
507
507
|
not to read, reporting both; `--local` forces the folder brain. On a brain in a
|
|
508
508
|
FOLDER it also names what it left out — notes a deny name, a `.memgignore` rule
|
|
509
|
-
or a symlink kept from the index,
|
|
510
|
-
|
|
511
|
-
reindex cannot say that: the rules are
|
|
512
|
-
the server only ever saw what arrived.
|
|
509
|
+
or a symlink kept from the index, any folder a rule excluded whole, and any other
|
|
510
|
+
symlink, which is never followed — so "3 notes rebuilt" is never the whole
|
|
511
|
+
answer when a fourth was dropped. A hosted reindex cannot say that: the rules are
|
|
512
|
+
applied on the machine that pushed, so the server only ever saw what arrived.
|
|
513
513
|
|
|
514
514
|
Under `--json` that is `not_indexed` with `denied_by_name`, `denied_explicitly`,
|
|
515
|
-
`excluded_by_rule`, `skipped_symlinks
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
515
|
+
`excluded_by_rule`, `skipped_symlinks`, `excluded_folders` and
|
|
516
|
+
`skipped_other_symlinks`. **Read the count together with the last two**: a rule
|
|
517
|
+
that names a folder stops the walk at the folder, and a symlink is never
|
|
518
|
+
followed, so the notes in such a folder are never counted — `not_indexed: 0`
|
|
519
|
+
beside a non-empty `excluded_folders` or `skipped_other_symlinks` means the count
|
|
520
|
+
is a floor, not the answer. `push` reports the same set with its total under
|
|
521
|
+
`not_sent`. A `recall` that brings back nothing the user wrote — no cards, or
|
|
522
|
+
only templates — counts the same set by reason in both modes (beside notes it
|
|
523
|
+
did search, only when a note is among them) and prints this command, never a
|
|
524
|
+
path; what to tell the user then is in `memgineering-recall`.
|
|
525
|
+
`--rebuild`
|
|
520
526
|
exists for one situation only, where an embedding model changed the shape of its
|
|
521
527
|
answers without changing its name and the index has to be laid down again.
|
|
522
528
|
|
package/dist/index.js
CHANGED
|
@@ -134,12 +134,32 @@ var init_enums = __esm({
|
|
|
134
134
|
}
|
|
135
135
|
});
|
|
136
136
|
|
|
137
|
+
// ../../packages/contracts/src/refresh-race.ts
|
|
138
|
+
function isRefreshRace(body) {
|
|
139
|
+
if (typeof body !== "object" || body === null) return false;
|
|
140
|
+
const error = body.error;
|
|
141
|
+
if (typeof error !== "object" || error === null) return false;
|
|
142
|
+
const { code, flags } = error;
|
|
143
|
+
if (code !== "not_authenticated" || !Array.isArray(flags)) return false;
|
|
144
|
+
return flags.some(
|
|
145
|
+
(flag2) => typeof flag2 === "object" && flag2 !== null && flag2.code === REFRESH_RACE_FLAG
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
var REFRESH_RACE_FLAG;
|
|
149
|
+
var init_refresh_race = __esm({
|
|
150
|
+
"../../packages/contracts/src/refresh-race.ts"() {
|
|
151
|
+
"use strict";
|
|
152
|
+
REFRESH_RACE_FLAG = "refresh_rotated_by_another_request";
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
137
156
|
// ../../packages/contracts/src/errors.ts
|
|
138
157
|
import { z as z3 } from "zod";
|
|
139
158
|
var ErrorCodeSchema, ERROR_EXIT_CODES, ReviewFlagSchema, ApiErrorSchema;
|
|
140
159
|
var init_errors = __esm({
|
|
141
160
|
"../../packages/contracts/src/errors.ts"() {
|
|
142
161
|
"use strict";
|
|
162
|
+
init_refresh_race();
|
|
143
163
|
ErrorCodeSchema = z3.enum([
|
|
144
164
|
"generic_failure",
|
|
145
165
|
// 1
|
|
@@ -1159,21 +1179,33 @@ var init_api = __esm({
|
|
|
1159
1179
|
*/
|
|
1160
1180
|
content: z11.string().min(50).max(1e5),
|
|
1161
1181
|
/**
|
|
1162
|
-
*
|
|
1163
|
-
* reads from the frontmatter of `content`, which must
|
|
1164
|
-
* three
|
|
1182
|
+
* Optional, and refused when it disagrees. core-api stores the title,
|
|
1183
|
+
* description and type it reads from the frontmatter of `content`, which must
|
|
1184
|
+
* carry all three (the title may come from `name:`). A value here that
|
|
1185
|
+
* differs from the frontmatter's, compared trimmed, is refused as
|
|
1186
|
+
* `usage_error` (HTTP 400) with a hint naming the frontmatter key; absent
|
|
1187
|
+
* means no claim. A `type` the registry cannot publish at all (`scoped-rule`,
|
|
1188
|
+
* `mdc-rule`, `plugin`) names no key to write: its hint says the type cannot
|
|
1189
|
+
* be published yet and to drop `--type`. `kordis push` ≤0.15.2 sends
|
|
1190
|
+
* `--type` here, and writes `type: skill` into the frontmatter of a SKILL.md
|
|
1191
|
+
* or *.skill.md file, a folder or a .skill bundle that names no type — so
|
|
1192
|
+
* `--type hook` on one is refused, where until 2026-10 it published a skill.
|
|
1193
|
+
* Later versions send no `type`: they write `--type` into the frontmatter.
|
|
1165
1194
|
*/
|
|
1166
1195
|
title: z11.string().min(1).max(200).optional(),
|
|
1167
1196
|
description: z11.string().min(1).max(2048).optional(),
|
|
1168
1197
|
type: HarnessTypeSchema.optional(),
|
|
1169
1198
|
/**
|
|
1170
1199
|
* A new package is published under this slug instead of the one derived from
|
|
1171
|
-
* the frontmatter title. An update keeps the slug of the package it updates
|
|
1200
|
+
* the frontmatter title. An update keeps the slug of the package it updates:
|
|
1201
|
+
* a slug here that differs from the target's is refused (`usage_error`).
|
|
1172
1202
|
*/
|
|
1173
1203
|
slug: SlugSchema.optional(),
|
|
1174
1204
|
/**
|
|
1175
|
-
* When updating, the target full_slug
|
|
1176
|
-
*
|
|
1205
|
+
* When updating, the target full_slug; its presence is what makes a publish
|
|
1206
|
+
* an update (`PUT /api/packages/:u/:s` sets it from the path). Omitted for a
|
|
1207
|
+
* new package, whose slug is `slug` or else derived from the frontmatter
|
|
1208
|
+
* title — never derived on an update.
|
|
1177
1209
|
*/
|
|
1178
1210
|
target_full_slug: FullSlugSchema.optional(),
|
|
1179
1211
|
/**
|
|
@@ -1181,6 +1213,12 @@ var init_api = __esm({
|
|
|
1181
1213
|
*/
|
|
1182
1214
|
bump: z11.enum(["patch", "minor", "major"]).optional(),
|
|
1183
1215
|
changelog: z11.string().max(2e3).optional(),
|
|
1216
|
+
/**
|
|
1217
|
+
* Refused when non-empty (`usage_error`): a package's dependencies are read
|
|
1218
|
+
* from its frontmatter's `depends:` and `recommends:` lists only. Kept in the
|
|
1219
|
+
* schema so the server sees them to refuse them — this object is non-strict,
|
|
1220
|
+
* and an unlisted key would be stripped without a word.
|
|
1221
|
+
*/
|
|
1184
1222
|
depends: z11.array(z11.string()).optional(),
|
|
1185
1223
|
recommends: z11.array(z11.string()).optional(),
|
|
1186
1224
|
/**
|
|
@@ -1680,9 +1718,122 @@ var init_telemetry = __esm({
|
|
|
1680
1718
|
});
|
|
1681
1719
|
|
|
1682
1720
|
// ../../packages/contracts/src/review-patterns.ts
|
|
1721
|
+
var STAGE_1_REJECT_PATTERNS, STAGE_1_5_AUP_PATTERNS, DETERMINISTIC_REJECT_PATTERNS;
|
|
1683
1722
|
var init_review_patterns = __esm({
|
|
1684
1723
|
"../../packages/contracts/src/review-patterns.ts"() {
|
|
1685
1724
|
"use strict";
|
|
1725
|
+
STAGE_1_REJECT_PATTERNS = [
|
|
1726
|
+
{
|
|
1727
|
+
code: "curl_pipe_shell",
|
|
1728
|
+
pattern: /\bcurl\s+[^|;]+\s*\|\s*(?:(?:sudo|doas)\s+)?(?:-E\s+|[A-Za-z_][A-Za-z0-9_]*=\S*\s+)*(?:sh|bash|zsh|ksh|dash|csh|tcsh|fish)\b/i,
|
|
1729
|
+
hint: "Replace `curl \u2026 | bash` with a verifiable install path: download the script to a file, show the user `cat` of it for inspection, then `bash <file>` only after confirmation."
|
|
1730
|
+
},
|
|
1731
|
+
{
|
|
1732
|
+
code: "wget_pipe_shell",
|
|
1733
|
+
pattern: /\bwget\s+[^|;]+\s*\|\s*(?:(?:sudo|doas)\s+)?(?:-E\s+|[A-Za-z_][A-Za-z0-9_]*=\S*\s+)*(?:sh|bash|zsh|ksh|dash|csh|tcsh|fish)\b/i,
|
|
1734
|
+
hint: "Replace `wget \u2026 | bash` with a verifiable install path: download the script to a file, show the user `cat` of it for inspection, then `bash <file>` only after confirmation."
|
|
1735
|
+
},
|
|
1736
|
+
{
|
|
1737
|
+
code: "shell_process_substitution",
|
|
1738
|
+
pattern: /(?:bash|sh|zsh)\s*<\(\s*(?:curl|wget)\b/i,
|
|
1739
|
+
hint: "Replace `bash <(curl \u2026)` with a verifiable install path: download the script to a file, show the user `cat` of it for inspection, then `bash <file>` only after confirmation."
|
|
1740
|
+
},
|
|
1741
|
+
{
|
|
1742
|
+
code: "eval_atob",
|
|
1743
|
+
pattern: /\beval\s*\(\s*atob\b/i,
|
|
1744
|
+
hint: "Encoded eval (`eval(atob(...))`) is a hallmark of obfuscated payloads. Move the logic into plain code the reader can audit."
|
|
1745
|
+
},
|
|
1746
|
+
{
|
|
1747
|
+
code: "eval_buffer_from",
|
|
1748
|
+
pattern: /\beval\s*\(\s*Buffer\.from\b/i,
|
|
1749
|
+
hint: "Encoded eval (`eval(Buffer.from(...))`) is a hallmark of obfuscated payloads. Move the logic into plain code the reader can audit."
|
|
1750
|
+
},
|
|
1751
|
+
{
|
|
1752
|
+
code: "aws_access_key",
|
|
1753
|
+
pattern: /\b(?:AKIA|ASIA|AGPA)[0-9A-Z]{16}\b/,
|
|
1754
|
+
hint: "Remove the AWS access key from the body. Reference credentials via env var (`AWS_ACCESS_KEY_ID`); rotate the leaked key immediately if it was real."
|
|
1755
|
+
},
|
|
1756
|
+
{
|
|
1757
|
+
code: "private_key_block",
|
|
1758
|
+
pattern: /-----BEGIN\s+(?:[A-Z0-9]+\s+)?PRIVATE\s+KEY(?:\s+BLOCK)?-----/,
|
|
1759
|
+
hint: "Remove the private key block from the body. Reference keys via env var or a secret manager; rotate the leaked key immediately if it was real."
|
|
1760
|
+
},
|
|
1761
|
+
{
|
|
1762
|
+
code: "google_api_key",
|
|
1763
|
+
pattern: /\bAIza[0-9A-Za-z_-]{35}\b/,
|
|
1764
|
+
hint: "Remove the Google API key from the body. Reference credentials via env var; rotate the leaked key immediately if it was real."
|
|
1765
|
+
},
|
|
1766
|
+
{
|
|
1767
|
+
code: "slack_token",
|
|
1768
|
+
pattern: /\b(?:xox[abpors]|xapp|xoxe)-[0-9a-zA-Z-]{10,}\b/,
|
|
1769
|
+
hint: "Remove the Slack token from the body. Reference tokens via env var; revoke the leaked token immediately if it was real."
|
|
1770
|
+
},
|
|
1771
|
+
{
|
|
1772
|
+
code: "github_token",
|
|
1773
|
+
pattern: /\b(?:gh[pousr]_[A-Za-z0-9_]{36,}|github_pat_[A-Za-z0-9_]{60,})\b/,
|
|
1774
|
+
hint: "Remove the GitHub token from the body. Reference tokens via env var; revoke the leaked token immediately if it was real."
|
|
1775
|
+
}
|
|
1776
|
+
];
|
|
1777
|
+
STAGE_1_5_AUP_PATTERNS = [
|
|
1778
|
+
{
|
|
1779
|
+
// Stolen-card / bank-credential trafficking storefronts. Anchored to the
|
|
1780
|
+
// trafficking jargon that only appears in carding markets, not in the
|
|
1781
|
+
// defensive "how to detect carding" literature. `dumps` is also an
|
|
1782
|
+
// ordinary developer verb — "this hook dumps vendor files", "dumps store
|
|
1783
|
+
// state", "dumps market data to CSV" all matched the shared noun list —
|
|
1784
|
+
// so after `dumps` only sale framing (`shop`, `for sale`) counts, while
|
|
1785
|
+
// `cvv` and `fullz` keep the full list. Narrowed when comments started
|
|
1786
|
+
// running this table (2026-10-14).
|
|
1787
|
+
code: "aup_stolen_financial_data_market",
|
|
1788
|
+
pattern: /\b(?:cvv|fullz)\s+(?:shop|store|for\s+sale|market|vendor)\b|\bdumps\s+(?:shop|for\s+sale)\b|\bdumps?\s+with\s+pin\b|\bsell(?:ing)?\s+(?:cvv|fullz|dumps)\b/i,
|
|
1789
|
+
hint: "This reads as a storefront for trafficking stolen financial data, which violates the Kordis AUP. If the package is about DETECTING or DEFENDING against card fraud, reword it so it does not read as an offer to buy/sell stolen data."
|
|
1790
|
+
},
|
|
1791
|
+
{
|
|
1792
|
+
// DDoS-for-hire (booter/stresser) services sold on a subscription.
|
|
1793
|
+
code: "aup_ddos_for_hire",
|
|
1794
|
+
pattern: /\b(?:booter|stresser)\s+(?:service|panel|subscription)\b|\b(?:ddos|dstat)\s+for\s+hire\b|\bhire\s+(?:a\s+)?(?:booter|stresser|ddos)\b/i,
|
|
1795
|
+
hint: "This reads as a DDoS-for-hire (booter/stresser) service, which violates the Kordis AUP. Load-testing tooling is fine \u2014 describe it as testing infrastructure you own/are authorized to test, not a for-hire attack service."
|
|
1796
|
+
},
|
|
1797
|
+
{
|
|
1798
|
+
// Malware/stealer/ransomware sold or distributed as a product ("builder
|
|
1799
|
+
// for sale", "buy our stealer"). A skill that DEFENDS against these, or a
|
|
1800
|
+
// malware-analysis skill, does not use buy/sell framing.
|
|
1801
|
+
code: "aup_malware_sale",
|
|
1802
|
+
pattern: /\b(?:ransomware|stealer|infostealer|rat|keylogger|botnet)\s+(?:builder|kit|panel|source)\s+(?:for\s+sale|for\s+rent)\b|\b(?:buy|sell(?:ing)?|rent)\s+(?:our\s+)?(?:ransomware|stealer|infostealer|botnet)\b/i,
|
|
1803
|
+
hint: "This reads as an offer to sell/rent malware, which violates the Kordis AUP. Malware ANALYSIS and DEFENSE content is welcome \u2014 reword so it does not read as distributing a weaponized build."
|
|
1804
|
+
},
|
|
1805
|
+
{
|
|
1806
|
+
// Child sexual abuse material. Unlike the rest of this table this entry is
|
|
1807
|
+
// NOT anchored to solicitation: it matches the spelled-out phrases ("child
|
|
1808
|
+
// porn", "child pornography", "child sexual abuse material") wherever they
|
|
1809
|
+
// appear, child-safety and moderation education included. Only the
|
|
1810
|
+
// acronym "CSAM" on its own does not match ("how platforms detect CSAM
|
|
1811
|
+
// for NCMEC reporting" passes). Kept this broad on purpose (2026-10-14):
|
|
1812
|
+
// comments have no Stage-2 review after this table, so a miss costs more
|
|
1813
|
+
// than a false positive.
|
|
1814
|
+
code: "aup_csam",
|
|
1815
|
+
pattern: /\bchild\s+(?:porn(?:ography)?|sexual\s+abuse\s+material)\b/i,
|
|
1816
|
+
hint: "This reads as solicitation or distribution of child sexual abuse material, which is illegal and violates the Kordis AUP. This is a terminal, non-appealable rejection."
|
|
1817
|
+
},
|
|
1818
|
+
{
|
|
1819
|
+
// Drug / untraceable-weapon brokering with explicit no-id / untraceable /
|
|
1820
|
+
// dark-web fulfillment language — not a bare mention of a substance or
|
|
1821
|
+
// firearm topic (e.g. harm-reduction or policy content does not use this
|
|
1822
|
+
// buy/sell + untraceable-fulfillment framing together).
|
|
1823
|
+
code: "aup_drug_weapon_brokering",
|
|
1824
|
+
pattern: /\b(?:buy|sell(?:ing)?)\b[\s\S]{0,40}\b(?:cocaine|heroin|fentanyl|methamphetamine|untraceable firearms?|untraceable guns?)\b[\s\S]{0,40}\b(?:no id|untraceable|worldwide shipping|dark ?web)\b/i,
|
|
1825
|
+
hint: "This reads as brokering illegal drugs or untraceable weapons for sale, which violates the Kordis AUP. If the package is about DETECTING or REPORTING such activity, reword it so it does not read as an offer to buy/sell."
|
|
1826
|
+
},
|
|
1827
|
+
{
|
|
1828
|
+
// A directive to dox or swat a specific, named/tagged target — not a
|
|
1829
|
+
// bare mention of doxxing/swatting as a topic (e.g. "how to protect
|
|
1830
|
+
// yourself from doxxing" does not match).
|
|
1831
|
+
code: "aup_targeted_harassment_directive",
|
|
1832
|
+
pattern: /\b(?:dox|swat)\s+(?:this (?:guy|girl|person|user)|him|her|@[\w.]+)\b/i,
|
|
1833
|
+
hint: "This reads as a directive to dox or swat a specific target, which violates the Kordis AUP. Remove the targeted-harassment instruction."
|
|
1834
|
+
}
|
|
1835
|
+
];
|
|
1836
|
+
DETERMINISTIC_REJECT_PATTERNS = [...STAGE_1_REJECT_PATTERNS, ...STAGE_1_5_AUP_PATTERNS];
|
|
1686
1837
|
}
|
|
1687
1838
|
});
|
|
1688
1839
|
|
|
@@ -7882,13 +8033,18 @@ async function useClient() {
|
|
|
7882
8033
|
} catch (err) {
|
|
7883
8034
|
const code = err.code;
|
|
7884
8035
|
if (code === "authorization_pending") {
|
|
7885
|
-
if (debug)
|
|
8036
|
+
if (debug)
|
|
8037
|
+
process.stderr.write(
|
|
8038
|
+
"[kordis] auto-finalize: authorization_pending (keeping pending)\n"
|
|
8039
|
+
);
|
|
7886
8040
|
} else {
|
|
7887
8041
|
await clearPending();
|
|
7888
8042
|
if (debug) {
|
|
7889
8043
|
const msg = err instanceof Error ? err.message : String(err);
|
|
7890
|
-
process.stderr.write(
|
|
7891
|
-
`)
|
|
8044
|
+
process.stderr.write(
|
|
8045
|
+
`[kordis] auto-finalize: poll failed (code=${code ?? "none"}): ${msg}
|
|
8046
|
+
`
|
|
8047
|
+
);
|
|
7892
8048
|
}
|
|
7893
8049
|
void reportError("auto_finalize_stage1_fail", err);
|
|
7894
8050
|
}
|
|
@@ -7912,7 +8068,8 @@ async function useClient() {
|
|
|
7912
8068
|
cfg = { ...cfg, username, sessionId: pollResult.session_id };
|
|
7913
8069
|
await saveCliConfig(cfg);
|
|
7914
8070
|
await clearPending();
|
|
7915
|
-
if (debug)
|
|
8071
|
+
if (debug)
|
|
8072
|
+
process.stderr.write(`[kordis] auto-finalize: persisted tokens for ${username}
|
|
7916
8073
|
`);
|
|
7917
8074
|
} catch (err) {
|
|
7918
8075
|
await clearPending();
|
|
@@ -7941,31 +8098,51 @@ async function useClient() {
|
|
|
7941
8098
|
}
|
|
7942
8099
|
} : {}
|
|
7943
8100
|
});
|
|
8101
|
+
const renewTokens = async (user, sent) => {
|
|
8102
|
+
try {
|
|
8103
|
+
const refreshed = await api.refreshSession(sent);
|
|
8104
|
+
currentTokens = {
|
|
8105
|
+
access_token: refreshed.access_token,
|
|
8106
|
+
refresh_token: refreshed.refresh_token,
|
|
8107
|
+
session_id: refreshed.session_id,
|
|
8108
|
+
expires_at_epoch: Math.floor(Date.now() / 1e3) + refreshed.expires_in
|
|
8109
|
+
};
|
|
8110
|
+
await keychain.save(user, currentTokens);
|
|
8111
|
+
await saveCliConfig({ ...cfg, sessionId: refreshed.session_id });
|
|
8112
|
+
return true;
|
|
8113
|
+
} catch (refreshErr) {
|
|
8114
|
+
if (!lostRefreshRace(refreshErr)) return false;
|
|
8115
|
+
const stored = await storedPairOtherThan(user, sent);
|
|
8116
|
+
if (stored === null) return false;
|
|
8117
|
+
currentTokens = stored;
|
|
8118
|
+
return true;
|
|
8119
|
+
}
|
|
8120
|
+
};
|
|
7944
8121
|
const authedRequest = async (fn) => {
|
|
7945
8122
|
try {
|
|
7946
8123
|
return await fn(api);
|
|
7947
8124
|
} catch (err) {
|
|
7948
|
-
|
|
7949
|
-
|
|
7950
|
-
const refreshed = await api.refreshSession(currentTokens.refresh_token);
|
|
7951
|
-
currentTokens = {
|
|
7952
|
-
access_token: refreshed.access_token,
|
|
7953
|
-
refresh_token: refreshed.refresh_token,
|
|
7954
|
-
session_id: refreshed.session_id,
|
|
7955
|
-
expires_at_epoch: Math.floor(Date.now() / 1e3) + refreshed.expires_in
|
|
7956
|
-
};
|
|
7957
|
-
await keychain.save(username, currentTokens);
|
|
7958
|
-
await saveCliConfig({ ...cfg, sessionId: refreshed.session_id });
|
|
7959
|
-
return await fn(api);
|
|
7960
|
-
} catch {
|
|
7961
|
-
throw err;
|
|
7962
|
-
}
|
|
7963
|
-
}
|
|
7964
|
-
throw err;
|
|
8125
|
+
const renewed = err instanceof KordisApiError && err.code === "not_authenticated" && currentTokens !== null && username !== null && await renewTokens(username, currentTokens.refresh_token);
|
|
8126
|
+
if (!renewed) throw err;
|
|
7965
8127
|
}
|
|
8128
|
+
return fn(api);
|
|
7966
8129
|
};
|
|
7967
8130
|
return { api, keychain, username, apiUrl: cfg.apiUrl, authedRequest };
|
|
7968
8131
|
}
|
|
8132
|
+
function lostRefreshRace(err) {
|
|
8133
|
+
return err instanceof KordisApiError && err.status === 401 && isRefreshRace({ error: { code: err.code, flags: err.flags } });
|
|
8134
|
+
}
|
|
8135
|
+
var RACE_REREADS_AT_MS = [500, 1e3, 2e3];
|
|
8136
|
+
async function storedPairOtherThan(username, sent) {
|
|
8137
|
+
let waited = 0;
|
|
8138
|
+
for (const at of RACE_REREADS_AT_MS) {
|
|
8139
|
+
await new Promise((resolve6) => setTimeout(resolve6, at - waited));
|
|
8140
|
+
waited = at;
|
|
8141
|
+
const stored = await createKeychain().then((store) => store.load(username)).catch(() => null);
|
|
8142
|
+
if (stored !== null && stored.refresh_token !== sent) return stored;
|
|
8143
|
+
}
|
|
8144
|
+
return null;
|
|
8145
|
+
}
|
|
7969
8146
|
|
|
7970
8147
|
// src/ui.ts
|
|
7971
8148
|
import kleur from "kleur";
|
|
@@ -9287,6 +9464,7 @@ function lintForLeaks(sources) {
|
|
|
9287
9464
|
}
|
|
9288
9465
|
|
|
9289
9466
|
// src/commands/push.ts
|
|
9467
|
+
init_src();
|
|
9290
9468
|
var collect = (val, acc) => acc.concat(val);
|
|
9291
9469
|
function toLineage(raws, kind) {
|
|
9292
9470
|
return (raws ?? []).map((raw) => {
|
|
@@ -9301,7 +9479,10 @@ function toLineage(raws, kind) {
|
|
|
9301
9479
|
var ASSET_MAX_BYTES = 2 * 1024 * 1024;
|
|
9302
9480
|
var BUNDLE_MAX_BYTES = 5 * 1024 * 1024;
|
|
9303
9481
|
function pushCommand() {
|
|
9304
|
-
return new Command4("push").description("publish a new package or a new version").argument("<path>", "SKILL.md file, .skill ZIP, or skill directory").option("--bump <strategy>", "patch|minor|major (update only)", "patch").option("--slug <slug>", 'override slug; accepts "user/slug" to point at a specific package').option(
|
|
9482
|
+
return new Command4("push").description("publish a new package or a new version").argument("<path>", "SKILL.md file, .skill ZIP, or skill directory").option("--bump <strategy>", "patch|minor|major (update only)", "patch").option("--slug <slug>", 'override slug; accepts "user/slug" to point at a specific package').option(
|
|
9483
|
+
"--type <type>",
|
|
9484
|
+
"type to write into a frontmatter that names none (else a SKILL.md, folder or .skill is a skill); refused if the frontmatter names another"
|
|
9485
|
+
).option("--changelog <text>", "optional changelog entry").option("--dry-run", "validate only; do not publish").option("--no-wait", "return immediately without waiting for review").option(
|
|
9305
9486
|
"--adapted-from <slug>",
|
|
9306
9487
|
"source this package was adapted from (repeatable); records lineage",
|
|
9307
9488
|
collect,
|
|
@@ -9316,17 +9497,15 @@ function pushCommand() {
|
|
|
9316
9497
|
let contentLabel = "SKILL.md";
|
|
9317
9498
|
if (s.isDirectory()) {
|
|
9318
9499
|
({ content, assets } = await bundleDirectory(abs));
|
|
9319
|
-
content =
|
|
9500
|
+
content = resolveType(content, opts.type, "skill");
|
|
9320
9501
|
} else if (abs.endsWith(".skill")) {
|
|
9321
9502
|
({ content, assets } = await bundleSkillZip(abs));
|
|
9322
|
-
content =
|
|
9503
|
+
content = resolveType(content, opts.type, "skill");
|
|
9323
9504
|
} else {
|
|
9324
9505
|
content = await readFile6(abs, "utf8");
|
|
9325
9506
|
contentLabel = basename(abs);
|
|
9326
9507
|
const isSkillFilename = abs.endsWith("/SKILL.md") || abs.endsWith(".skill.md");
|
|
9327
|
-
|
|
9328
|
-
content = ensureTypeField(content, "skill");
|
|
9329
|
-
}
|
|
9508
|
+
content = resolveType(content, opts.type, isSkillFilename ? "skill" : void 0);
|
|
9330
9509
|
}
|
|
9331
9510
|
let nameFromFm = null;
|
|
9332
9511
|
try {
|
|
@@ -9406,7 +9585,6 @@ function pushCommand() {
|
|
|
9406
9585
|
bump: opts.bump,
|
|
9407
9586
|
slug: opts.slug && !opts.slug.includes("/") ? opts.slug : void 0,
|
|
9408
9587
|
target_full_slug: targetFullSlug,
|
|
9409
|
-
type: opts.type,
|
|
9410
9588
|
changelog: opts.changelog,
|
|
9411
9589
|
lineage: lineage.length > 0 ? lineage : void 0,
|
|
9412
9590
|
dry_run: !!opts.dryRun
|
|
@@ -9542,15 +9720,39 @@ function printLeakWarnings(findings) {
|
|
|
9542
9720
|
printHuman(c.yellow(` - ${f.message}`));
|
|
9543
9721
|
}
|
|
9544
9722
|
}
|
|
9545
|
-
function
|
|
9723
|
+
function resolveType(content, typeFlag, inferred) {
|
|
9724
|
+
if (typeFlag === void 0) return inferred ? ensureTypeField(content, inferred) : content;
|
|
9725
|
+
const declared = frontmatterType(content);
|
|
9726
|
+
if (declared === void 0) return ensureTypeField(content, typeFlag);
|
|
9727
|
+
if (declared !== typeFlag) {
|
|
9728
|
+
throw new KordisApiError(400, {
|
|
9729
|
+
error: {
|
|
9730
|
+
code: "usage_error",
|
|
9731
|
+
message: `--type "${typeFlag}" disagrees with the type in the frontmatter, "${declared}"`,
|
|
9732
|
+
hint: `This file's frontmatter says \`type: ${declared}\`; change it or drop \`--type\`.`
|
|
9733
|
+
}
|
|
9734
|
+
});
|
|
9735
|
+
}
|
|
9736
|
+
return content;
|
|
9737
|
+
}
|
|
9738
|
+
function frontmatterType(content) {
|
|
9739
|
+
try {
|
|
9740
|
+
const value = parseFrontmatter(content).frontmatter?.["type"];
|
|
9741
|
+
return typeof value === "string" ? value : void 0;
|
|
9742
|
+
} catch {
|
|
9743
|
+
return void 0;
|
|
9744
|
+
}
|
|
9745
|
+
}
|
|
9746
|
+
function ensureTypeField(content, type) {
|
|
9546
9747
|
const fmMatch = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content);
|
|
9547
9748
|
if (!fmMatch) return content;
|
|
9548
9749
|
const fmBlock = fmMatch[1];
|
|
9549
9750
|
if (/^type\s*:/m.test(fmBlock)) return content;
|
|
9550
|
-
const
|
|
9551
|
-
|
|
9751
|
+
const scalar = HarnessTypeSchema.safeParse(type).success ? type : JSON.stringify(type);
|
|
9752
|
+
const injected = fmMatch[0].replace(/^---\r?\n/, () => `---
|
|
9753
|
+
type: ${scalar}
|
|
9552
9754
|
`);
|
|
9553
|
-
return content.replace(fmMatch[0], injected);
|
|
9755
|
+
return content.replace(fmMatch[0], () => injected);
|
|
9554
9756
|
}
|
|
9555
9757
|
var EXCLUDE_NAMES = /* @__PURE__ */ new Set([".DS_Store", "Thumbs.db", ".gitkeep"]);
|
|
9556
9758
|
var EXCLUDE_DIRS = /* @__PURE__ */ new Set(["__pycache__", "node_modules", ".git", "evals", "dist", ".turbo"]);
|
|
@@ -10226,45 +10428,31 @@ function getTranslator(type, format) {
|
|
|
10226
10428
|
return fn;
|
|
10227
10429
|
}
|
|
10228
10430
|
|
|
10229
|
-
// src/
|
|
10230
|
-
init_brand();
|
|
10231
|
-
import { join as join9 } from "path";
|
|
10232
|
-
import { readFile as readFile7, writeFile as writeFile5, mkdir as mkdir5 } from "fs/promises";
|
|
10233
|
-
function kordisHome2() {
|
|
10234
|
-
return brandHome();
|
|
10235
|
-
}
|
|
10236
|
-
function lockfilePath() {
|
|
10237
|
-
return join9(kordisHome2(), "installed.json");
|
|
10238
|
-
}
|
|
10239
|
-
function lockfileDisplayPath() {
|
|
10240
|
-
const path2 = lockfilePath();
|
|
10241
|
-
const home = process.env["HOME"];
|
|
10242
|
-
return home && path2.startsWith(home) ? "~" + path2.slice(home.length) : path2;
|
|
10243
|
-
}
|
|
10244
|
-
async function readLockfile() {
|
|
10245
|
-
try {
|
|
10246
|
-
const raw = await readFile7(lockfilePath(), "utf8");
|
|
10247
|
-
return JSON.parse(raw);
|
|
10248
|
-
} catch {
|
|
10249
|
-
return { packages: {} };
|
|
10250
|
-
}
|
|
10251
|
-
}
|
|
10252
|
-
async function writeLockfile(lf) {
|
|
10253
|
-
await mkdir5(kordisHome2(), { recursive: true });
|
|
10254
|
-
await writeFile5(lockfilePath(), JSON.stringify(lf, null, 2) + "\n", {
|
|
10255
|
-
encoding: "utf8",
|
|
10256
|
-
mode: 384
|
|
10257
|
-
});
|
|
10258
|
-
}
|
|
10259
|
-
async function recordInstalled(fullSlug, entry) {
|
|
10260
|
-
const lf = await readLockfile();
|
|
10261
|
-
lf.packages[fullSlug] = entry;
|
|
10262
|
-
await writeLockfile(lf);
|
|
10263
|
-
}
|
|
10264
|
-
|
|
10265
|
-
// src/lib/merge-emitted-file.ts
|
|
10431
|
+
// ../../packages/translators/src/merge-emitted-file.ts
|
|
10266
10432
|
import tomlLib2 from "@iarna/toml";
|
|
10267
10433
|
import { parse as yamlParse, stringify as yamlStringify } from "yaml";
|
|
10434
|
+
var MergeRefusal = class extends Error {
|
|
10435
|
+
code;
|
|
10436
|
+
reason;
|
|
10437
|
+
constructor(reason) {
|
|
10438
|
+
super(refusalMessage(reason));
|
|
10439
|
+
this.name = "MergeRefusal";
|
|
10440
|
+
this.code = reason.kind === "prototype_key" ? "unsafe_package_config" : "merge_refused";
|
|
10441
|
+
this.reason = reason;
|
|
10442
|
+
}
|
|
10443
|
+
};
|
|
10444
|
+
function refusalMessage(reason) {
|
|
10445
|
+
switch (reason.kind) {
|
|
10446
|
+
case "prototype_key":
|
|
10447
|
+
return `refusing to merge config fragment \u2014 prototype-polluting key "${reason.key}" at ${reason.path}`;
|
|
10448
|
+
case "malformed_file":
|
|
10449
|
+
return `refusing to merge into "${reason.targetPath}" \u2014 existing file is not valid ${reason.format.toUpperCase()}: ${reason.parseError}`;
|
|
10450
|
+
case "non_object_root":
|
|
10451
|
+
return `refusing to merge into "${reason.targetPath}" \u2014 existing ${reason.format.toUpperCase()} root is not an object`;
|
|
10452
|
+
case "dangling_marker":
|
|
10453
|
+
return `refusing to merge \u2014 found a "kordis:${reason.fullSlug}" start marker with no matching end marker`;
|
|
10454
|
+
}
|
|
10455
|
+
}
|
|
10268
10456
|
function isPlainObject(v) {
|
|
10269
10457
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
10270
10458
|
}
|
|
@@ -10289,11 +10477,7 @@ function assertNoProtoKeys(value, path2) {
|
|
|
10289
10477
|
if (!isPlainObject(value)) return;
|
|
10290
10478
|
for (const [k, v] of Object.entries(value)) {
|
|
10291
10479
|
if (PROTO_KEYS.has(k)) {
|
|
10292
|
-
|
|
10293
|
-
`refusing to merge config fragment \u2014 prototype-polluting key "${k}" at ${[...path2, k].join(".")}`
|
|
10294
|
-
);
|
|
10295
|
-
e.hint = "The package emits a config key named __proto__/constructor/prototype, which no legitimate tool config needs. Report it to the author if you did not publish it.";
|
|
10296
|
-
throw e;
|
|
10480
|
+
throw new MergeRefusal({ kind: "prototype_key", key: k, path: [...path2, k].join(".") });
|
|
10297
10481
|
}
|
|
10298
10482
|
assertNoProtoKeys(v, [...path2, k]);
|
|
10299
10483
|
}
|
|
@@ -10302,19 +10486,15 @@ function isOwnedServerLeaf(path2) {
|
|
|
10302
10486
|
return path2.length === 2 && (path2[0] === "mcpServers" || path2[0] === "mcp_servers");
|
|
10303
10487
|
}
|
|
10304
10488
|
function deepMergeFragment(existing, fragment, path2 = []) {
|
|
10305
|
-
|
|
10489
|
+
assertNoProtoKeys(fragment, path2);
|
|
10490
|
+
return mergeAt(existing, fragment, path2);
|
|
10491
|
+
}
|
|
10492
|
+
function mergeAt(existing, fragment, path2) {
|
|
10306
10493
|
if (isOwnedServerLeaf(path2)) return fragment;
|
|
10307
10494
|
if (isPlainObject(existing) && isPlainObject(fragment)) {
|
|
10308
10495
|
const out = { ...existing };
|
|
10309
10496
|
for (const [k, v] of Object.entries(fragment)) {
|
|
10310
|
-
|
|
10311
|
-
const e = new Error(
|
|
10312
|
-
`refusing to merge config fragment \u2014 prototype-polluting key "${k}" at ${[...path2, k].join(".")}`
|
|
10313
|
-
);
|
|
10314
|
-
e.hint = "The package emits a config key named __proto__/constructor/prototype, which no legitimate tool config needs. Report it to the author if you did not publish it.";
|
|
10315
|
-
throw e;
|
|
10316
|
-
}
|
|
10317
|
-
out[k] = deepMergeFragment(existing[k], v, [...path2, k]);
|
|
10497
|
+
out[k] = mergeAt(existing[k], v, [...path2, k]);
|
|
10318
10498
|
}
|
|
10319
10499
|
return out;
|
|
10320
10500
|
}
|
|
@@ -10357,18 +10537,15 @@ function mergeStructuredFile(existingRaw, fragmentRaw, format, targetPath) {
|
|
|
10357
10537
|
try {
|
|
10358
10538
|
existing = parseStructured(existingRaw, format);
|
|
10359
10539
|
} catch (err) {
|
|
10360
|
-
|
|
10361
|
-
|
|
10362
|
-
|
|
10363
|
-
|
|
10364
|
-
|
|
10540
|
+
throw new MergeRefusal({
|
|
10541
|
+
kind: "malformed_file",
|
|
10542
|
+
targetPath,
|
|
10543
|
+
format,
|
|
10544
|
+
parseError: err.message
|
|
10545
|
+
});
|
|
10365
10546
|
}
|
|
10366
10547
|
if (!isPlainObject(existing) && isPlainObject(fragment)) {
|
|
10367
|
-
|
|
10368
|
-
`refusing to merge into "${targetPath}" \u2014 existing ${format.toUpperCase()} root is not an object`
|
|
10369
|
-
);
|
|
10370
|
-
e.hint = `${targetPath} parses but its top level is not an object, so a config fragment can't be merged in. Move the file aside (or fix its shape), then re-run \`kordis pull\`/\`kordis update\`.`;
|
|
10371
|
-
throw e;
|
|
10548
|
+
throw new MergeRefusal({ kind: "non_object_root", targetPath, format });
|
|
10372
10549
|
}
|
|
10373
10550
|
const merged = deepMergeFragment(existing, fragment);
|
|
10374
10551
|
return stringifyStructured(merged, format);
|
|
@@ -10390,15 +10567,67 @@ function mergeFencedFile(existingRaw, fullSlug, block) {
|
|
|
10390
10567
|
return existingRaw.slice(0, startMatch.index) + trimmedBlock + existingRaw.slice(spanEnd);
|
|
10391
10568
|
}
|
|
10392
10569
|
if (startMatch && endIdx === -1) {
|
|
10393
|
-
|
|
10394
|
-
`refusing to merge \u2014 found a "kordis:${fullSlug}" start marker with no matching end marker`
|
|
10395
|
-
);
|
|
10396
|
-
e.hint = `The file has a dangling <!-- kordis:${fullSlug}:...:start --> marker. Delete that line (or restore its <!-- kordis:${fullSlug}:end --> line), then re-run \`kordis pull\`/\`kordis update\`.`;
|
|
10397
|
-
throw e;
|
|
10570
|
+
throw new MergeRefusal({ kind: "dangling_marker", fullSlug });
|
|
10398
10571
|
}
|
|
10399
10572
|
return existingRaw.trimEnd() + "\n\n" + trimmedBlock + "\n";
|
|
10400
10573
|
}
|
|
10401
10574
|
|
|
10575
|
+
// src/lockfile.ts
|
|
10576
|
+
init_brand();
|
|
10577
|
+
import { join as join9 } from "path";
|
|
10578
|
+
import { readFile as readFile7, writeFile as writeFile5, mkdir as mkdir5 } from "fs/promises";
|
|
10579
|
+
function kordisHome2() {
|
|
10580
|
+
return brandHome();
|
|
10581
|
+
}
|
|
10582
|
+
function lockfilePath() {
|
|
10583
|
+
return join9(kordisHome2(), "installed.json");
|
|
10584
|
+
}
|
|
10585
|
+
function lockfileDisplayPath() {
|
|
10586
|
+
const path2 = lockfilePath();
|
|
10587
|
+
const home = process.env["HOME"];
|
|
10588
|
+
return home && path2.startsWith(home) ? "~" + path2.slice(home.length) : path2;
|
|
10589
|
+
}
|
|
10590
|
+
async function readLockfile() {
|
|
10591
|
+
try {
|
|
10592
|
+
const raw = await readFile7(lockfilePath(), "utf8");
|
|
10593
|
+
return JSON.parse(raw);
|
|
10594
|
+
} catch {
|
|
10595
|
+
return { packages: {} };
|
|
10596
|
+
}
|
|
10597
|
+
}
|
|
10598
|
+
async function writeLockfile(lf) {
|
|
10599
|
+
await mkdir5(kordisHome2(), { recursive: true });
|
|
10600
|
+
await writeFile5(lockfilePath(), JSON.stringify(lf, null, 2) + "\n", {
|
|
10601
|
+
encoding: "utf8",
|
|
10602
|
+
mode: 384
|
|
10603
|
+
});
|
|
10604
|
+
}
|
|
10605
|
+
async function recordInstalled(fullSlug, entry) {
|
|
10606
|
+
const lf = await readLockfile();
|
|
10607
|
+
lf.packages[fullSlug] = entry;
|
|
10608
|
+
await writeLockfile(lf);
|
|
10609
|
+
}
|
|
10610
|
+
|
|
10611
|
+
// src/lib/merge-refusal.ts
|
|
10612
|
+
function adaptMergeRefusal(err) {
|
|
10613
|
+
if (!(err instanceof MergeRefusal)) return null;
|
|
10614
|
+
const e = new Error(err.message);
|
|
10615
|
+
e.hint = mergeRefusalHint(err.reason);
|
|
10616
|
+
return e;
|
|
10617
|
+
}
|
|
10618
|
+
function mergeRefusalHint(reason) {
|
|
10619
|
+
switch (reason.kind) {
|
|
10620
|
+
case "prototype_key":
|
|
10621
|
+
return "The package emits a config key named __proto__/constructor/prototype, which no legitimate tool config needs. Report it to the author if you did not publish it.";
|
|
10622
|
+
case "malformed_file":
|
|
10623
|
+
return `Fix or remove the malformed ${reason.targetPath}, then re-run \`kordis pull\`/\`kordis update\`. Refusing to overwrite it blind \u2014 it may hold config from other tools/packages.`;
|
|
10624
|
+
case "non_object_root":
|
|
10625
|
+
return `${reason.targetPath} parses but its top level is not an object, so a config fragment can't be merged in. Move the file aside (or fix its shape), then re-run \`kordis pull\`/\`kordis update\`.`;
|
|
10626
|
+
case "dangling_marker":
|
|
10627
|
+
return `The file has a dangling <!-- kordis:${reason.fullSlug}:...:start --> marker. Delete that line (or restore its <!-- kordis:${reason.fullSlug}:end --> line), then re-run \`kordis pull\`/\`kordis update\`.`;
|
|
10628
|
+
}
|
|
10629
|
+
}
|
|
10630
|
+
|
|
10402
10631
|
// src/commands/pull.ts
|
|
10403
10632
|
async function requireExecutableApproval(pkg, fullSlug, files, approve2) {
|
|
10404
10633
|
if (!pkg.capabilities.executable) return;
|
|
@@ -10796,7 +11025,12 @@ async function applyEmittedFile(abs, f, fullSlug, notes) {
|
|
|
10796
11025
|
if (err.code !== "ENOENT") throw err;
|
|
10797
11026
|
}
|
|
10798
11027
|
if (f.format === "json" || f.format === "yaml" || f.format === "toml") {
|
|
10799
|
-
|
|
11028
|
+
let merged2;
|
|
11029
|
+
try {
|
|
11030
|
+
merged2 = mergeStructuredFile(existingRaw, f.content, f.format, abs);
|
|
11031
|
+
} catch (err) {
|
|
11032
|
+
throw adaptMergeRefusal(err) ?? err;
|
|
11033
|
+
}
|
|
10800
11034
|
if (existingRaw !== null && existingRaw.trim() !== "") {
|
|
10801
11035
|
notes?.push(
|
|
10802
11036
|
`merged into ${f.path} \u2014 comments/formatting in the existing file are not preserved`
|
|
@@ -10805,11 +11039,17 @@ async function applyEmittedFile(abs, f, fullSlug, notes) {
|
|
|
10805
11039
|
await writeFile6(abs, merged2, "utf8");
|
|
10806
11040
|
return;
|
|
10807
11041
|
}
|
|
10808
|
-
|
|
11042
|
+
let merged;
|
|
11043
|
+
try {
|
|
11044
|
+
merged = mergeFencedFile(existingRaw, fullSlug, f.content);
|
|
11045
|
+
} catch (err) {
|
|
11046
|
+
throw adaptMergeRefusal(err) ?? err;
|
|
11047
|
+
}
|
|
10809
11048
|
await writeFile6(abs, merged, "utf8");
|
|
10810
11049
|
}
|
|
10811
11050
|
|
|
10812
11051
|
// src/commands/read.ts
|
|
11052
|
+
init_src2();
|
|
10813
11053
|
init_sections();
|
|
10814
11054
|
import { Command as Command6 } from "commander";
|
|
10815
11055
|
|
|
@@ -10829,6 +11069,15 @@ function untrustedFenceNotice(sentinel) {
|
|
|
10829
11069
|
}
|
|
10830
11070
|
|
|
10831
11071
|
// src/commands/read.ts
|
|
11072
|
+
var shellWord = (word) => /^[\w./@:+-]+$/.test(word) ? word : JSON.stringify(word);
|
|
11073
|
+
function readAgainCommand(fullSlug, parsed, opts) {
|
|
11074
|
+
const suffix = parsed.version ? `@${parsed.version}` : parsed.range ? `@${parsed.range}` : "";
|
|
11075
|
+
return [
|
|
11076
|
+
`kordis read ${shellWord(`${fullSlug}${suffix}`)}`,
|
|
11077
|
+
...opts.pin === void 0 ? [] : [`--pin ${shellWord(opts.pin)}`],
|
|
11078
|
+
...opts.allowPending ? ["--allow-pending"] : []
|
|
11079
|
+
].join(" ");
|
|
11080
|
+
}
|
|
10832
11081
|
function readCommand() {
|
|
10833
11082
|
return new Command6("read").description("fetch a package body into the agent context (no install)").argument("<full_slug>", "package identifier {username}/{slug} or {username}/{slug}@{version}").option("--allow-pending", "allow versions whose AI review has not passed", false).option("--pin <semver>", "pin to an exact version (overrides @suffix)").option("--outline", "print only the section map (skeleton) \u2014 cheap on context", false).option("--section <name>", "print only one section (see names with --outline)").action(
|
|
10834
11083
|
async (rawSlug, opts) => {
|
|
@@ -10906,9 +11155,19 @@ Bundled files \u2014 fetch with ${c.cyan(`kordis pull ${res.full_slug}`)}:
|
|
|
10906
11155
|
const sec = sectionOf(res.body, opts.section);
|
|
10907
11156
|
if (!sec) {
|
|
10908
11157
|
const names = outlineOf(res.body).sections.map((s) => s.name).join(", ");
|
|
11158
|
+
const readAgain = readAgainCommand(res.full_slug, parsed, opts);
|
|
11159
|
+
if (isJsonMode()) {
|
|
11160
|
+
throw new KordisApiError(400, {
|
|
11161
|
+
error: {
|
|
11162
|
+
code: "usage_error",
|
|
11163
|
+
message: `section "${opts.section}" not found`,
|
|
11164
|
+
hint: names ? `Available sections: ${names}. Read one with \`${readAgain} --section <name>\`.` : `This package has no sub-sections \u2014 read the whole body with \`${readAgain}\`.`
|
|
11165
|
+
}
|
|
11166
|
+
});
|
|
11167
|
+
}
|
|
10909
11168
|
process.stderr.write(
|
|
10910
11169
|
`${c.red("\u2717")} section "${opts.section}" not found. Available: ${names || "(none \u2014 this package has no sub-sections)"}
|
|
10911
|
-
See the map with ${c.cyan(
|
|
11170
|
+
See the map with ${c.cyan(`${readAgain} --outline`)}.
|
|
10912
11171
|
`
|
|
10913
11172
|
);
|
|
10914
11173
|
process.exitCode = 2;
|
package/package.json
CHANGED