knowingfyi 0.3.0 → 0.3.1
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/README.md +3 -1
- package/knowing-skills.mjs +174 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -55,7 +55,7 @@ Publishing and source-grounded docs are also available through `publish`, `sync`
|
|
|
55
55
|
npx knowingfyi login --site https://your-knowing-host
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
The browser approval stores a customer token locally
|
|
58
|
+
The browser approval stores a customer token locally until revoked, and the commands send it to that host. On another machine or in CI, set `KNOWING_CUSTOMER_TOKEN` with `KNOWING_SITE` naming the same host. `logout` removes the local token; revoke the terminal under Authorized devices to disable it. Customer-token reading, `knowing token`, `--install-only` and the `KNOWING_SITE` check arrive with the release after 0.1.1.
|
|
59
59
|
|
|
60
60
|
## When your Knowing host changes
|
|
61
61
|
|
|
@@ -66,3 +66,5 @@ Update `KNOWING_SITE`, MCP server URLs and any vendored workflow project URLs de
|
|
|
66
66
|
The CLI rejects API redirects, and publishing/install secrets have different capabilities from customer logins. Existing self-hosted and local origins remain supported. Operators can use the [platform domain cutover runbook](https://github.com/BrickspaceLab/knowing/blob/main/docs/knowing/platform-domain-cutover.md) to plan compatibility and rollback.
|
|
67
67
|
|
|
68
68
|
Node.js 24 or newer is required. See [authoring skills and source configuration](https://github.com/BrickspaceLab/knowing/blob/main/docs/knowing/release-source.md).
|
|
69
|
+
|
|
70
|
+
Interactive native plugin installation installs and registers available free skills first, then opens account authorization and waits for paid access before upgrading the same pinned plugin and registering it again. `--free` skips the upgrade; CI and install-token runs never wait for browser checkout. Locally modified plugin files are preserved. This flow requires the next npm CLI release.
|
package/knowing-skills.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { pathToFileURL } from "node:url";
|
|
|
6
6
|
import {
|
|
7
7
|
mkdir,
|
|
8
8
|
readFile,
|
|
9
|
+
readdir,
|
|
9
10
|
writeFile,
|
|
10
11
|
lstat,
|
|
11
12
|
realpath,
|
|
@@ -184,9 +185,43 @@ async function safeDirectory(directory) {
|
|
|
184
185
|
throw new Error(`Unsafe directory: ${current}`);
|
|
185
186
|
}
|
|
186
187
|
}
|
|
188
|
+
async function identicalSkill(directory, files, prefix) {
|
|
189
|
+
const pending = [directory];
|
|
190
|
+
const expected = new Map(
|
|
191
|
+
files
|
|
192
|
+
.filter((file) => file.path.startsWith(prefix))
|
|
193
|
+
.map((file) => [file.path.slice(prefix.length), file]),
|
|
194
|
+
);
|
|
195
|
+
if ((await lstat(directory)).isSymbolicLink()) return false;
|
|
196
|
+
let count = 0;
|
|
197
|
+
while (pending.length) {
|
|
198
|
+
const current = pending.pop();
|
|
199
|
+
for (const entry of await readdir(current, { withFileTypes: true })) {
|
|
200
|
+
const target = path.join(current, entry.name);
|
|
201
|
+
if (entry.isSymbolicLink()) return false;
|
|
202
|
+
if (entry.isDirectory()) {
|
|
203
|
+
pending.push(target);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const file = expected.get(
|
|
207
|
+
path.relative(directory, target).split(path.sep).join("/"),
|
|
208
|
+
);
|
|
209
|
+
if (!entry.isFile() || !file) return false;
|
|
210
|
+
const content = Buffer.from(
|
|
211
|
+
file.content,
|
|
212
|
+
file.encoding === "base64" ? "base64" : "utf8",
|
|
213
|
+
);
|
|
214
|
+
if (!(await readFile(target)).equals(content)) return false;
|
|
215
|
+
count++;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return count === expected.size;
|
|
219
|
+
}
|
|
187
220
|
export async function installArtifact(artifact, options = {}) {
|
|
188
221
|
const cwd = await realpath(path.resolve(options.cwd ?? process.cwd()));
|
|
189
222
|
const created = [];
|
|
223
|
+
const reused = new Set();
|
|
224
|
+
const backups = new Map();
|
|
190
225
|
try {
|
|
191
226
|
if (["skills", "project-skill", "space-skill"].includes(artifact.format)) {
|
|
192
227
|
const base = path.join(
|
|
@@ -205,12 +240,34 @@ export async function installArtifact(artifact, options = {}) {
|
|
|
205
240
|
if (!names.length) throw new Error("The release contains no skills.");
|
|
206
241
|
for (const name of names) {
|
|
207
242
|
const dest = path.join(base, name);
|
|
208
|
-
|
|
209
|
-
|
|
243
|
+
try {
|
|
244
|
+
await mkdir(dest);
|
|
245
|
+
created.push(dest);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
if (error.code !== "EEXIST" || !options.reuseIdentical) throw error;
|
|
248
|
+
const prefix = `skills/${name}/`;
|
|
249
|
+
if (await identicalSkill(dest, artifact.files, prefix)) {
|
|
250
|
+
reused.add(dest);
|
|
251
|
+
} else if (
|
|
252
|
+
options.previousArtifact?.files.some((file) =>
|
|
253
|
+
file.path.startsWith(prefix),
|
|
254
|
+
) &&
|
|
255
|
+
(await identicalSkill(dest, options.previousArtifact.files, prefix))
|
|
256
|
+
) {
|
|
257
|
+
const backup = `${dest}.upgrade-${randomBytes(12).toString("hex")}`;
|
|
258
|
+
await rename(dest, backup);
|
|
259
|
+
backups.set(dest, backup);
|
|
260
|
+
await mkdir(dest);
|
|
261
|
+
created.push(dest);
|
|
262
|
+
} else {
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
210
266
|
}
|
|
211
267
|
for (const file of artifact.files.filter((f) =>
|
|
212
268
|
f.path.startsWith("skills/"),
|
|
213
269
|
)) {
|
|
270
|
+
if (reused.has(path.join(base, file.path.split("/")[1]))) continue;
|
|
214
271
|
const dest = path.join(base, file.path.slice(7));
|
|
215
272
|
await safeDirectory(path.dirname(dest));
|
|
216
273
|
await writeFile(
|
|
@@ -229,9 +286,27 @@ export async function installArtifact(artifact, options = {}) {
|
|
|
229
286
|
);
|
|
230
287
|
await safeDirectory(base);
|
|
231
288
|
const dest = path.join(base, artifact.format);
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
289
|
+
try {
|
|
290
|
+
await mkdir(dest);
|
|
291
|
+
created.push(dest);
|
|
292
|
+
} catch (error) {
|
|
293
|
+
if (error.code !== "EEXIST" || !options.reuseIdentical) throw error;
|
|
294
|
+
if (await identicalSkill(dest, artifact.files, "")) {
|
|
295
|
+
reused.add(dest);
|
|
296
|
+
} else if (
|
|
297
|
+
options.previousArtifact &&
|
|
298
|
+
(await identicalSkill(dest, options.previousArtifact.files, ""))
|
|
299
|
+
) {
|
|
300
|
+
const backup = `${dest}.upgrade-${randomBytes(12).toString("hex")}`;
|
|
301
|
+
await rename(dest, backup);
|
|
302
|
+
backups.set(dest, backup);
|
|
303
|
+
await mkdir(dest);
|
|
304
|
+
created.push(dest);
|
|
305
|
+
} else {
|
|
306
|
+
throw error;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
for (const file of reused.has(dest) ? [] : artifact.files) {
|
|
235
310
|
const output = path.join(dest, file.path);
|
|
236
311
|
await safeDirectory(path.dirname(output));
|
|
237
312
|
await writeFile(
|
|
@@ -241,10 +316,14 @@ export async function installArtifact(artifact, options = {}) {
|
|
|
241
316
|
);
|
|
242
317
|
}
|
|
243
318
|
}
|
|
244
|
-
|
|
319
|
+
for (const backup of backups.values())
|
|
320
|
+
await rm(backup, { recursive: true }).catch(() => {});
|
|
321
|
+
return [...created, ...reused];
|
|
245
322
|
} catch (error) {
|
|
246
323
|
for (const directory of created.reverse())
|
|
247
324
|
await rm(directory, { recursive: true, force: true });
|
|
325
|
+
for (const [destination, backup] of backups)
|
|
326
|
+
await rename(backup, destination);
|
|
248
327
|
if (error.code === "EEXIST")
|
|
249
328
|
throw new Error(
|
|
250
329
|
"A destination already exists. Existing files were preserved; choose another project or remove the old installation yourself.",
|
|
@@ -646,7 +725,7 @@ async function setupRelease(command, target, flag, switches) {
|
|
|
646
725
|
}
|
|
647
726
|
if (requiresToken)
|
|
648
727
|
print(
|
|
649
|
-
`Set the KNOWING_INSTALL_TOKEN repository secret to an install-only token with access to this release:\nknowing login --site ${origin} --install-only | gh secret set KNOWING_INSTALL_TOKEN\nIt only installs releases, never reads your projects or uses hosted answers,
|
|
728
|
+
`Set the KNOWING_INSTALL_TOKEN repository secret to an install-only token with access to this release:\nknowing login --site ${origin} --install-only | gh secret set KNOWING_INSTALL_TOKEN\nIt only installs releases, never reads your projects or uses hosted answers, stays active until revoked from Authorized devices.`,
|
|
650
729
|
);
|
|
651
730
|
print(
|
|
652
731
|
`Created ${file}\nRelease ${release.version}, task ${setup.task.name}. Commit and push after reviewing the workflow.`,
|
|
@@ -680,6 +759,23 @@ async function setupRelease(command, target, flag, switches) {
|
|
|
680
759
|
),
|
|
681
760
|
expected,
|
|
682
761
|
);
|
|
762
|
+
const freeExpected = { ...expected, tier: "free" };
|
|
763
|
+
let previousArtifact;
|
|
764
|
+
if (
|
|
765
|
+
!options.free &&
|
|
766
|
+
(options.premium || (release.tier ?? 0) > 0) &&
|
|
767
|
+
!setup.skill
|
|
768
|
+
) {
|
|
769
|
+
try {
|
|
770
|
+
previousArtifact = validateArtifact(
|
|
771
|
+
await rpc(origin, "artifact", freeExpected, false),
|
|
772
|
+
freeExpected,
|
|
773
|
+
);
|
|
774
|
+
} catch (error) {
|
|
775
|
+
// A paid-only or private release has no anonymous free installation to upgrade.
|
|
776
|
+
if (!["FORBIDDEN", "NOT_FOUND"].includes(error.code)) throw error;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
683
779
|
// Create the MCP file first so an existing configuration cannot leave a partial skill installation.
|
|
684
780
|
let mcpFile;
|
|
685
781
|
if (command === "add-agent" && setup.format === "skills" && release.mcp) {
|
|
@@ -698,7 +794,11 @@ async function setupRelease(command, target, flag, switches) {
|
|
|
698
794
|
}
|
|
699
795
|
let directories;
|
|
700
796
|
try {
|
|
701
|
-
directories = await installArtifact(artifact, {
|
|
797
|
+
directories = await installArtifact(artifact, {
|
|
798
|
+
agent: setup.agent,
|
|
799
|
+
reuseIdentical: true,
|
|
800
|
+
previousArtifact,
|
|
801
|
+
});
|
|
702
802
|
} catch (error) {
|
|
703
803
|
if (mcpFile) await rm(mcpFile);
|
|
704
804
|
throw error;
|
|
@@ -710,11 +810,20 @@ async function setupRelease(command, target, flag, switches) {
|
|
|
710
810
|
command === "add-skill" &&
|
|
711
811
|
!setup.skill &&
|
|
712
812
|
!options.free &&
|
|
813
|
+
!options.premium &&
|
|
713
814
|
!release.fullAccess
|
|
714
|
-
)
|
|
815
|
+
) {
|
|
816
|
+
const upgradeUrl = releaseAccessUrl(origin, release.releaseId);
|
|
715
817
|
print(
|
|
716
|
-
`
|
|
818
|
+
`Free skills installed. Compare paid tiers and their benefits: ${upgradeUrl}`,
|
|
717
819
|
);
|
|
820
|
+
if (
|
|
821
|
+
!switches.noBrowser &&
|
|
822
|
+
!process.env.CI &&
|
|
823
|
+
!process.env.KNOWING_INSTALL_TOKEN
|
|
824
|
+
)
|
|
825
|
+
openBrowser(upgradeUrl);
|
|
826
|
+
}
|
|
718
827
|
const registrationDirectory = await preparePluginRegistration(
|
|
719
828
|
artifact,
|
|
720
829
|
directories[0],
|
|
@@ -734,6 +843,40 @@ async function setupRelease(command, target, flag, switches) {
|
|
|
734
843
|
}
|
|
735
844
|
print("Plugin registered. Restart your coding agent to load it.");
|
|
736
845
|
}
|
|
846
|
+
if (
|
|
847
|
+
command === "add-agent" &&
|
|
848
|
+
artifact.format.endsWith("-plugin") &&
|
|
849
|
+
!options.free &&
|
|
850
|
+
!options.premium &&
|
|
851
|
+
!release.fullAccess &&
|
|
852
|
+
// Only a free installation can be replaced; a paid tier's files differ from it.
|
|
853
|
+
!((release.tier ?? 0) > 0) &&
|
|
854
|
+
process.stdin.isTTY &&
|
|
855
|
+
!process.env.CI &&
|
|
856
|
+
!process.env.KNOWING_INSTALL_TOKEN
|
|
857
|
+
) {
|
|
858
|
+
print(
|
|
859
|
+
"Free plugin installed. Continue in your browser to sign in and compare paid tiers. The plugin upgrades automatically after purchase.",
|
|
860
|
+
);
|
|
861
|
+
try {
|
|
862
|
+
await setupRelease(
|
|
863
|
+
command,
|
|
864
|
+
target,
|
|
865
|
+
(name) =>
|
|
866
|
+
name === "agent"
|
|
867
|
+
? setup.agent
|
|
868
|
+
: name === "version"
|
|
869
|
+
? release.version
|
|
870
|
+
: flag(name),
|
|
871
|
+
{ ...switches, premium: true },
|
|
872
|
+
);
|
|
873
|
+
} catch (error) {
|
|
874
|
+
// A failed upgrade rolls back its files; the free installation above already succeeded.
|
|
875
|
+
print(
|
|
876
|
+
`Paid upgrade did not finish: ${error.message} Compare paid tiers: ${releaseAccessUrl(origin, release.releaseId)}`,
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
737
880
|
return;
|
|
738
881
|
} catch (error) {
|
|
739
882
|
if (!["FORBIDDEN", "UNAUTHORIZED"].includes(error.code)) {
|
|
@@ -811,9 +954,9 @@ async function setupRelease(command, target, flag, switches) {
|
|
|
811
954
|
}
|
|
812
955
|
}
|
|
813
956
|
}
|
|
814
|
-
/**
|
|
957
|
+
/** Public plan comparison; authentication happens when acquiring access. */
|
|
815
958
|
export function releaseAccessUrl(origin, releaseId) {
|
|
816
|
-
return `${origin}/
|
|
959
|
+
return `${origin}/skills/releases/${encodeURIComponent(releaseId)}`;
|
|
817
960
|
}
|
|
818
961
|
/**
|
|
819
962
|
* Polls an install's access while the buyer can still act in the browser,
|
|
@@ -857,11 +1000,21 @@ export async function preparePluginRegistration(artifact, directory) {
|
|
|
857
1000
|
).content,
|
|
858
1001
|
);
|
|
859
1002
|
catalog.plugins[0].source.path = `./${path.basename(directory)}`;
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
1003
|
+
const file = ".agents/plugins/marketplace.json";
|
|
1004
|
+
const content = JSON.stringify(catalog, null, 2);
|
|
1005
|
+
const destination = path.join(root, file);
|
|
1006
|
+
try {
|
|
1007
|
+
const stat = await lstat(destination);
|
|
1008
|
+
if (
|
|
1009
|
+
!stat.isSymbolicLink() &&
|
|
1010
|
+
stat.isFile() &&
|
|
1011
|
+
(await readFile(destination, "utf8")) === content
|
|
1012
|
+
)
|
|
1013
|
+
return root;
|
|
1014
|
+
} catch (error) {
|
|
1015
|
+
if (error.code !== "ENOENT") throw error;
|
|
1016
|
+
}
|
|
1017
|
+
await writeSetupFile(file, content, root);
|
|
865
1018
|
return root;
|
|
866
1019
|
}
|
|
867
1020
|
export async function registerPlugin(artifact, directory, run = runFile) {
|
|
@@ -994,7 +1147,7 @@ export async function main(args) {
|
|
|
994
1147
|
);
|
|
995
1148
|
print(result.token);
|
|
996
1149
|
note(
|
|
997
|
-
"Store this install-only token as the KNOWING_INSTALL_TOKEN repository secret. It isn't saved on this machine and
|
|
1150
|
+
"Store this install-only token as the KNOWING_INSTALL_TOKEN repository secret. It isn't saved on this machine and stays active until revoked.",
|
|
998
1151
|
);
|
|
999
1152
|
return;
|
|
1000
1153
|
}
|
|
@@ -1002,7 +1155,9 @@ export async function main(args) {
|
|
|
1002
1155
|
await saveConfiguration(file, { token: result.token });
|
|
1003
1156
|
const defaults = path.join(path.dirname(file), "default-site.json");
|
|
1004
1157
|
await saveConfiguration(defaults, { origin });
|
|
1005
|
-
print(
|
|
1158
|
+
print(
|
|
1159
|
+
"Connected. Your token is stored locally and stays active until revoked.",
|
|
1160
|
+
);
|
|
1006
1161
|
return;
|
|
1007
1162
|
}
|
|
1008
1163
|
throw new Error("Login expired. Start login again.");
|