@michinarai/cli 0.1.1 → 0.2.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 CHANGED
@@ -62,6 +62,7 @@ node scripts/build.mjs && narai build
62
62
  ```sh
63
63
  narai dev --locale ja --task task.json # task.json: the task Narai would start it on ({id, kind, assist, prompt})
64
64
  narai dev --preset preset.json --port 4000
65
+ narai dev --grant microphone # as a step that grants the microphone: only if capabilities.microphone is true
65
66
  ```
66
67
 
67
68
  **`narai login`** signs in to Narai in the browser. The answer comes back to this machine only (127.0.0.1), and the
@@ -9,6 +9,8 @@ export default class Dev extends NaraiCommand<typeof Dev> {
9
9
  locale: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
10
  task: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
11
  preset: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ grant: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ "no-open": import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
14
  };
13
15
  run(): Promise<void>;
14
16
  }
@@ -3,6 +3,7 @@ import { resolve } from "node:path";
3
3
  import { Flags } from "@oclif/core";
4
4
  import { startHarness } from "@michinarai/sdk/harness";
5
5
  import { NaraiCommand } from "../base-command.js";
6
+ import { deps } from "../lib/deps.js";
6
7
  import { NaraiError } from "../lib/errors.js";
7
8
  import { checkedManifest, toolRoot } from "../lib/project.js";
8
9
  async function json(path, what) {
@@ -15,16 +16,28 @@ async function json(path, what) {
15
16
  throw new NaraiError(`${what} ${path}: ${error.message}`, "bad_input");
16
17
  }
17
18
  }
19
+ /** The task a step would start the tool on, for its first answer kind: none when it answers nothing. */
20
+ function sampleTask(manifest) {
21
+ const kind = Object.keys(manifest.answers ?? {})[0];
22
+ if (!kind)
23
+ return undefined;
24
+ return { id: "dev_task", kind, assist: "allowed", prompt: `A sample task (${kind}): in a lesson, Narai writes the prompt.` };
25
+ }
18
26
  export default class Dev extends NaraiCommand {
19
27
  static summary = "Run the tool in the local harness, mounted as a lesson mounts it.";
20
- static description = "Narai's host on http://localhost, the tool on http://127.0.0.1 in a sandboxed iframe: two origins, the real handshake, every message checked against the manifest. Open the host's address in a browser; stop with Ctrl-C.";
28
+ static description = "Narai's host on http://localhost, the tool on http://127.0.0.1 in a sandboxed iframe: two origins, the real handshake, every message checked against the manifest. It opens the host's address in your browser; stop with Ctrl-C. Without --task the tool starts on a sample task of the manifest's first answer kind. It runs on this machine only: Narai's lessons see a tool once it's registered and pushed (narai login, narai tools register, narai tools push).";
21
29
  static flags = {
22
30
  dir: Flags.string({ description: "The tool's built files (served at the embed URL's path).", default: "dist" }),
23
31
  port: Flags.integer({ description: "The host page's port (default: any free one)." }),
24
32
  "tool-port": Flags.integer({ description: "The tool's port (default: any free one)." }),
25
33
  locale: Flags.string({ description: "The locale the tool starts in (default: its first)." }),
26
- task: Flags.string({ description: "A JSON file: the task Narai would start it on ({id, kind, assist, prompt})." }),
34
+ task: Flags.string({ description: "A JSON file: the task Narai would start it on ({id, kind, assist, prompt}). Default: a sample task of the manifest's first answer kind." }),
27
35
  preset: Flags.string({ description: "A JSON file: the step's preset payload ({schema, ...})." }),
36
+ grant: Flags.string({
37
+ description: "What the step grants the tool, as Narai's start does: only what the manifest's capabilities declare.",
38
+ options: ["microphone"], multiple: true,
39
+ }),
40
+ "no-open": Flags.boolean({ description: "Print the host's address; don't open it." }),
28
41
  };
29
42
  async run() {
30
43
  const root = await toolRoot(process.cwd());
@@ -38,12 +51,29 @@ export default class Dev extends NaraiCommand {
38
51
  options.locale = this.flags.locale;
39
52
  const task = await json(this.flags.task, "task");
40
53
  const preset = await json(this.flags.preset, "preset");
41
- if (task)
42
- options.task = task;
54
+ // no --task: a sample of what a step starts it on, so the tool's answer has something to answer
55
+ const started = task ?? sampleTask(manifest);
56
+ if (started)
57
+ options.task = started;
43
58
  if (preset)
44
59
  options.preset = preset;
60
+ // Narai grants only what the manifest declares and drops the rest: so does this, saying so
61
+ const asked = (this.flags.grant ?? []);
62
+ const granted = asked.filter((g) => manifest.capabilities?.[g] === true);
63
+ for (const g of asked)
64
+ if (!granted.includes(g))
65
+ this.out.progress(`not granted: ${g} (the manifest's capabilities.${g} isn't true)`);
66
+ options.granted = granted;
45
67
  const harness = await startHarness(options);
46
- this.out.result({ host: harness.hostUrl, tool: harness.toolUrl }, () => [`host ${harness.hostUrl}`, `tool ${harness.toolUrl}`]);
68
+ const kind = options.task?.kind ?? null;
69
+ this.out.result({ host: harness.hostUrl, tool: harness.toolUrl, task: kind, granted }, () => [
70
+ `Open ${harness.hostUrl} in a browser: your tool, mounted as a lesson mounts it, and what it sends Narai.`,
71
+ `task ${kind ?? "none (the manifest answers nothing)"}${task ? "" : kind ? " (a sample; --task <file> starts another)" : ""}`,
72
+ ...(granted.length ? [`granted ${granted.join(", ")}`] : []),
73
+ "This runs on this machine only. To use the tool in a Narai lesson: narai login, narai tools register, narai tools push.",
74
+ ]);
75
+ if (!this.out.json && !this.flags["no-open"])
76
+ deps.openBrowser(harness.hostUrl);
47
77
  this.out.progress("Ctrl-C to stop");
48
78
  await new Promise((done) => {
49
79
  const stop = () => { void harness.close().then(done); };
@@ -1 +1 @@
1
- {"version":3,"file":"dev.js","sourceRoot":"","sources":["../../src/commands/dev.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dev.js","sourceRoot":"","sources":["../../src/commands/dev.ts"],"names":[],"mappings":"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"}
@@ -4,7 +4,7 @@ import { NaraiError } from "../lib/errors.js";
4
4
  import { MANIFEST, checkedManifest, readManifest, toolRoot } from "../lib/project.js";
5
5
  export default class Validate extends NaraiCommand {
6
6
  static summary = `Check ${MANIFEST} against the contract, as Narai's registry does.`;
7
- static description = "The schema, then what a schema can't say: every payload a manifest names is declared, no answer payload carries a verdict, commands and answers need an embed that runs the SDK, files sorted by path.";
7
+ static description = "The schema (slugs are lower-case words joined by single hyphens), then what a schema can't say: every payload a manifest names is declared, a music.* payload Narai owns is declared with Narai's schema (musicPayloads), no answer or task payload carries a verdict, commands and answers need an embed that runs the SDK, <name>--<publisher> fits a DNS label, a relative embed url is one of the files, files sorted by path.";
8
8
  async run() {
9
9
  const root = await toolRoot(process.cwd());
10
10
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"validate.js","sourceRoot":"","sources":["../../src/commands/validate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAEtF,MAAM,CAAC,OAAO,OAAO,QAAS,SAAQ,YAA6B;IACjE,MAAM,CAAU,OAAO,GAAG,SAAS,QAAQ,kDAAkD,CAAC;IAC9F,MAAM,CAAU,WAAW,GAAG,wMAAwM,CAAC;IAEvO,KAAK,CAAC,GAAG;QACP,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;QAC3C,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,eAAe,CAAC,IAAI,CAAC,CAAC;YAC7C,MAAM,QAAQ,GAAG,gBAAgB,CAAC,MAAM,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC;YAC5D,KAAK,MAAM,CAAC,IAAI,QAAQ;gBAAE,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;YAC7D,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,QAAQ,CAAC,EAAE,EAAE,OAAO,EAAE,QAAQ,CAAC,OAAO,EAAE,QAAQ,EAAE,EAAE,GAAG,EAAE,CAAC,GAAG,QAAQ,CAAC,EAAE,IAAI,QAAQ,CAAC,OAAO,wBAAwB,CAAC,CAAC;QACxJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,KAAK,YAAY,UAAU,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC;gBAC5E,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI;oBAAE,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,UAAU,CAAa;wBAAE,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YACzG,CAAC;YACD,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC"}
1
+ {"version":3,"file":"validate.js","sourceRoot":"","sources":["../../src/commands/validate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAEtF,MAAM,CAAC,OAAO,OAAO,QAAS,SAAQ,YAA6B;IACjE,MAAM,CAAU,OAAO,GAAG,SAAS,QAAQ,kDAAkD,CAAC;IAC9F,MAAM,CAAU,WAAW,GAAG,oaAAoa,CAAC;IAEnc,KAAK,CAAC,GAAG;QACP,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;QAC3C,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,eAAe,CAAC,IAAI,CAAC,CAAC;YAC7C,MAAM,QAAQ,GAAG,gBAAgB,CAAC,MAAM,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC;YAC5D,KAAK,MAAM,CAAC,IAAI,QAAQ;gBAAE,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;YAC7D,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,QAAQ,CAAC,EAAE,EAAE,OAAO,EAAE,QAAQ,CAAC,OAAO,EAAE,QAAQ,EAAE,EAAE,GAAG,EAAE,CAAC,GAAG,QAAQ,CAAC,EAAE,IAAI,QAAQ,CAAC,OAAO,wBAAwB,CAAC,CAAC;QACxJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,KAAK,YAAY,UAAU,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC;gBAC5E,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI;oBAAE,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,UAAU,CAAa;wBAAE,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YACzG,CAAC;YACD,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC"}
@@ -205,6 +205,34 @@ export interface paths {
205
205
  patch?: never;
206
206
  trace?: never;
207
207
  };
208
+ "/api/tools/launches/{launch}/storage/{key}": {
209
+ parameters: {
210
+ query?: never;
211
+ header?: never;
212
+ path?: never;
213
+ cookie?: never;
214
+ };
215
+ /**
216
+ * Storage Get
217
+ * @description A key the tool kept in this launch.
218
+ */
219
+ get: operations["storage_get_api_tools_launches__launch__storage__key__get"];
220
+ /**
221
+ * Storage Put
222
+ * @description Keep a key in this launch, or rewrite it.
223
+ */
224
+ put: operations["storage_put_api_tools_launches__launch__storage__key__put"];
225
+ post?: never;
226
+ /**
227
+ * Storage Delete
228
+ * @description Remove a key from this launch (done whether or not it was there).
229
+ */
230
+ delete: operations["storage_delete_api_tools_launches__launch__storage__key__delete"];
231
+ options?: never;
232
+ head?: never;
233
+ patch?: never;
234
+ trace?: never;
235
+ };
208
236
  }
209
237
  export type webhooks = Record<string, never>;
210
238
  export interface components {
@@ -281,6 +309,11 @@ export interface components {
281
309
  /** Tokens */
282
310
  tokens: components["schemas"]["CiTokenOut"][];
283
311
  };
312
+ /** HTTPValidationError */
313
+ HTTPValidationError: {
314
+ /** Detail */
315
+ detail?: components["schemas"]["ValidationError"][];
316
+ };
284
317
  /** LaunchIn */
285
318
  LaunchIn: {
286
319
  /** Preset */
@@ -395,6 +428,19 @@ export interface components {
395
428
  /** Scopes */
396
429
  scopes: components["schemas"]["Scope"][];
397
430
  };
431
+ /** StorageIn */
432
+ StorageIn: {
433
+ /**
434
+ * Value
435
+ * @description Any JSON, at most 16384 bytes of it compact
436
+ */
437
+ value: unknown;
438
+ };
439
+ /** StorageOut */
440
+ StorageOut: {
441
+ /** Value */
442
+ value: unknown;
443
+ };
398
444
  /** ToolIn */
399
445
  ToolIn: {
400
446
  /** Name */
@@ -426,6 +472,19 @@ export interface components {
426
472
  /** Tools */
427
473
  tools: components["schemas"]["ToolOut"][];
428
474
  };
475
+ /** ValidationError */
476
+ ValidationError: {
477
+ /** Location */
478
+ loc: (string | number)[];
479
+ /** Message */
480
+ msg: string;
481
+ /** Error Type */
482
+ type: string;
483
+ /** Input */
484
+ input?: unknown;
485
+ /** Context */
486
+ ctx?: Record<string, never>;
487
+ };
429
488
  /** VersionDetail */
430
489
  VersionDetail: {
431
490
  /**
@@ -1496,4 +1555,100 @@ export interface operations {
1496
1555
  };
1497
1556
  };
1498
1557
  };
1558
+ storage_get_api_tools_launches__launch__storage__key__get: {
1559
+ parameters: {
1560
+ query?: never;
1561
+ header?: never;
1562
+ path: {
1563
+ launch: string;
1564
+ key: string;
1565
+ };
1566
+ cookie?: never;
1567
+ };
1568
+ requestBody?: never;
1569
+ responses: {
1570
+ /** @description Successful Response */
1571
+ 200: {
1572
+ headers: {
1573
+ [name: string]: unknown;
1574
+ };
1575
+ content: {
1576
+ "application/json": components["schemas"]["StorageOut"];
1577
+ };
1578
+ };
1579
+ /** @description Validation Error */
1580
+ 422: {
1581
+ headers: {
1582
+ [name: string]: unknown;
1583
+ };
1584
+ content: {
1585
+ "application/json": components["schemas"]["HTTPValidationError"];
1586
+ };
1587
+ };
1588
+ };
1589
+ };
1590
+ storage_put_api_tools_launches__launch__storage__key__put: {
1591
+ parameters: {
1592
+ query?: never;
1593
+ header?: never;
1594
+ path: {
1595
+ launch: string;
1596
+ key: string;
1597
+ };
1598
+ cookie?: never;
1599
+ };
1600
+ requestBody: {
1601
+ content: {
1602
+ "application/json": components["schemas"]["StorageIn"];
1603
+ };
1604
+ };
1605
+ responses: {
1606
+ /** @description Successful Response */
1607
+ 204: {
1608
+ headers: {
1609
+ [name: string]: unknown;
1610
+ };
1611
+ content?: never;
1612
+ };
1613
+ /** @description Validation Error */
1614
+ 422: {
1615
+ headers: {
1616
+ [name: string]: unknown;
1617
+ };
1618
+ content: {
1619
+ "application/json": components["schemas"]["HTTPValidationError"];
1620
+ };
1621
+ };
1622
+ };
1623
+ };
1624
+ storage_delete_api_tools_launches__launch__storage__key__delete: {
1625
+ parameters: {
1626
+ query?: never;
1627
+ header?: never;
1628
+ path: {
1629
+ launch: string;
1630
+ key: string;
1631
+ };
1632
+ cookie?: never;
1633
+ };
1634
+ requestBody?: never;
1635
+ responses: {
1636
+ /** @description Successful Response */
1637
+ 204: {
1638
+ headers: {
1639
+ [name: string]: unknown;
1640
+ };
1641
+ content?: never;
1642
+ };
1643
+ /** @description Validation Error */
1644
+ 422: {
1645
+ headers: {
1646
+ [name: string]: unknown;
1647
+ };
1648
+ content: {
1649
+ "application/json": components["schemas"]["HTTPValidationError"];
1650
+ };
1651
+ };
1652
+ };
1653
+ };
1499
1654
  }
@@ -18,7 +18,13 @@ async function entry(naraiUrl) {
18
18
  catch (error) {
19
19
  throw new KeychainUnavailableError(error);
20
20
  }
21
- return new keyring.AsyncEntry(keychainService(naraiUrl), ACCOUNT);
21
+ // a Linux machine with no Secret Service fails here, as the entry is made
22
+ try {
23
+ return new keyring.AsyncEntry(keychainService(naraiUrl), ACCOUNT);
24
+ }
25
+ catch (error) {
26
+ throw new KeychainUnavailableError(error);
27
+ }
22
28
  }
23
29
  async function guarded(action) {
24
30
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"credentials.js","sourceRoot":"","sources":["../../src/lib/credentials.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC;;mDAEmD;AACnD,MAAM,OAAO,GAAG,eAAe,CAAC;AAEhC,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,QAAgB,EAAU,EAAE,CAAC,yBAAyB,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC,IAAI,EAAE,CAAC;AAS/G,MAAM,wBAAyB,SAAQ,UAAU;IAC/C,YAAY,KAAc;QACxB,KAAK,CACH,oCAAoC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,8CAA8C,EACxI,sBAAsB,CACvB,CAAC;IACJ,CAAC;CACF;AAED,uFAAuF;AACvF,KAAK,UAAU,KAAK,CAAC,QAAgB;IACnC,IAAI,OAA0C,CAAC;IAC/C,IAAI,CAAC;QACH,OAAO,GAAG,MAAM,MAAM,CAAC,kBAAkB,CAAC,CAAC;IAC7C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,wBAAwB,CAAC,KAAK,CAAC,CAAC;IAC5C,CAAC;IACD,OAAO,IAAI,OAAO,CAAC,UAAU,CAAC,eAAe,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,CAAC;AACpE,CAAC;AAED,KAAK,UAAU,OAAO,CAAI,MAAwB;IAChD,IAAI,CAAC;QACH,OAAO,MAAM,MAAM,EAAE,CAAC;IACxB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,wBAAwB,CAAC,KAAK,CAAC,CAAC;IAC5C,CAAC;AACH,CAAC;AAED,MAAM,CAAC,MAAM,QAAQ,GAAoB;IACvC,KAAK,CAAC,GAAG,CAAC,QAAQ;QAChB,MAAM,CAAC,GAAG,MAAM,KAAK,CAAC,QAAQ,CAAC,CAAC;QAChC,OAAO,CAAC,MAAM,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,IAAI,IAAI,CAAC;IACxD,CAAC;IACD,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,KAAK;QACvB,MAAM,CAAC,GAAG,MAAM,KAAK,CAAC,QAAQ,CAAC,CAAC;QAChC,MAAM,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC;IAC5C,CAAC;IACD,KAAK,CAAC,MAAM,CAAC,QAAQ;QACnB,MAAM,CAAC,GAAG,MAAM,KAAK,CAAC,QAAQ,CAAC,CAAC;QAChC,OAAO,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,gBAAgB,EAAE,CAAC,CAAC;IAC7C,CAAC;CACF,CAAC"}
1
+ {"version":3,"file":"credentials.js","sourceRoot":"","sources":["../../src/lib/credentials.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC;;mDAEmD;AACnD,MAAM,OAAO,GAAG,eAAe,CAAC;AAEhC,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,QAAgB,EAAU,EAAE,CAAC,yBAAyB,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC,IAAI,EAAE,CAAC;AAS/G,MAAM,wBAAyB,SAAQ,UAAU;IAC/C,YAAY,KAAc;QACxB,KAAK,CACH,oCAAoC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,8CAA8C,EACxI,sBAAsB,CACvB,CAAC;IACJ,CAAC;CACF;AAED,uFAAuF;AACvF,KAAK,UAAU,KAAK,CAAC,QAAgB;IACnC,IAAI,OAA0C,CAAC;IAC/C,IAAI,CAAC;QACH,OAAO,GAAG,MAAM,MAAM,CAAC,kBAAkB,CAAC,CAAC;IAC7C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,wBAAwB,CAAC,KAAK,CAAC,CAAC;IAC5C,CAAC;IACD,0EAA0E;IAC1E,IAAI,CAAC;QACH,OAAO,IAAI,OAAO,CAAC,UAAU,CAAC,eAAe,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,CAAC;IACpE,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,wBAAwB,CAAC,KAAK,CAAC,CAAC;IAC5C,CAAC;AACH,CAAC;AAED,KAAK,UAAU,OAAO,CAAI,MAAwB;IAChD,IAAI,CAAC;QACH,OAAO,MAAM,MAAM,EAAE,CAAC;IACxB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,wBAAwB,CAAC,KAAK,CAAC,CAAC;IAC5C,CAAC;AACH,CAAC;AAED,MAAM,CAAC,MAAM,QAAQ,GAAoB;IACvC,KAAK,CAAC,GAAG,CAAC,QAAQ;QAChB,MAAM,CAAC,GAAG,MAAM,KAAK,CAAC,QAAQ,CAAC,CAAC;QAChC,OAAO,CAAC,MAAM,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,IAAI,IAAI,CAAC;IACxD,CAAC;IACD,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,KAAK;QACvB,MAAM,CAAC,GAAG,MAAM,KAAK,CAAC,QAAQ,CAAC,CAAC;QAChC,MAAM,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC;IAC5C,CAAC;IACD,KAAK,CAAC,MAAM,CAAC,QAAQ;QACnB,MAAM,CAAC,GAAG,MAAM,KAAK,CAAC,QAAQ,CAAC,CAAC;QAChC,OAAO,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,gBAAgB,EAAE,CAAC,CAAC;IAC7C,CAAC;CACF,CAAC"}
package/dist/lib/http.js CHANGED
@@ -5,7 +5,16 @@ export async function send(url, init = {}) {
5
5
  return await fetch(url, { redirect: "manual", signal: AbortSignal.timeout(30_000), ...init });
6
6
  }
7
7
  catch (error) {
8
- throw new NaraiError(`couldn't reach ${new URL(url).host}: ${error.message}`, "network");
8
+ throw new NaraiError(unreached(new URL(url).host, error), "network");
9
9
  }
10
10
  }
11
+ // Node's certificate errors: a self-signed one, or a server that leaves out its chain
12
+ const UNTRUSTED = /^(UNABLE_TO_VERIFY_LEAF_SIGNATURE|UNABLE_TO_GET_ISSUER_CERT(_LOCALLY)?|SELF_SIGNED_CERT_IN_CHAIN|DEPTH_ZERO_SELF_SIGNED_CERT|CERT_HAS_EXPIRED|ERR_TLS_CERT_ALTNAME_INVALID)$/;
13
+ /** fetch says only "fetch failed": the reason is its cause. */
14
+ function unreached(host, error) {
15
+ const cause = error.cause;
16
+ if (cause?.code && UNTRUSTED.test(cause.code))
17
+ return `${host}'s certificate isn't trusted (${cause.code}: ${cause.message ?? ""}); for a local Narai signed by your own CA, set NODE_EXTRA_CA_CERTS to that CA's file`;
18
+ return `couldn't reach ${host}: ${cause?.message ?? error.message}`;
19
+ }
11
20
  //# sourceMappingURL=http.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"http.js","sourceRoot":"","sources":["../../src/lib/http.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,+GAA+G;AAC/G,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,GAAW,EAAE,OAAoB,EAAE;IAC5D,IAAI,CAAC;QACH,OAAO,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,GAAG,IAAI,EAAE,CAAC,CAAC;IAChG,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,UAAU,CAAC,kBAAkB,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,KAAM,KAAe,CAAC,OAAO,EAAE,EAAE,SAAS,CAAC,CAAC;IACtG,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"http.js","sourceRoot":"","sources":["../../src/lib/http.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,+GAA+G;AAC/G,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,GAAW,EAAE,OAAoB,EAAE;IAC5D,IAAI,CAAC;QACH,OAAO,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,GAAG,IAAI,EAAE,CAAC,CAAC;IAChG,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,UAAU,CAAC,SAAS,CAAC,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,SAAS,CAAC,CAAC;IACvE,CAAC;AACH,CAAC;AAED,sFAAsF;AACtF,MAAM,SAAS,GAAG,8KAA8K,CAAC;AAEjM,+DAA+D;AAC/D,SAAS,SAAS,CAAC,IAAY,EAAE,KAAc;IAC7C,MAAM,KAAK,GAAI,KAAyD,CAAC,KAAK,CAAC;IAC/E,IAAI,KAAK,EAAE,IAAI,IAAI,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;QAAE,OAAO,GAAG,IAAI,iCAAiC,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,OAAO,IAAI,EAAE,uFAAuF,CAAC;IACxN,OAAO,kBAAkB,IAAI,KAAK,KAAK,EAAE,OAAO,IAAK,KAAe,CAAC,OAAO,EAAE,CAAC;AACjF,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@michinarai/cli",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "The narai command: scaffold, validate, build, run and register tools for Narai lessons.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -43,7 +43,7 @@
43
43
  },
44
44
  "dependencies": {
45
45
  "@inquirer/confirm": "6.3.3",
46
- "@michinarai/sdk": "0.2.1",
46
+ "@michinarai/sdk": "0.3.0",
47
47
  "@napi-rs/keyring": "2.1.1",
48
48
  "@oclif/core": "5.1.7",
49
49
  "openapi-fetch": "0.17.0"
@@ -15,6 +15,8 @@ learner: a tool never grades and never learns who the learner is.
15
15
  - With `assist: honors`, a task whose `assist` is `none` gets no hints and no self-checks from the tool.
16
16
  - Show the task's `prompt` as Narai sends it; it is already in the learner's language. Say `ready` in the locale the
17
17
  tool actually renders.
18
+ - The microphone is the step's to grant, launch by launch: declare `capabilities.microphone: true`, and show its
19
+ control only when `tool.start.granted` has `"microphone"`. `narai dev --grant microphone` simulates the grant.
18
20
  - Only the SDK talks to Narai (`connect`, `ready`, `answer`, `practice`, `onCommand`, `close`). No other `postMessage`,
19
21
  no network unless `capabilities.network` says so.
20
22
 
@@ -13,6 +13,8 @@
13
13
  <input id="text" autocomplete="off">
14
14
  <button id="say">Answer</button>
15
15
  <button id="drill">Practice</button>
16
+ <button id="mic" hidden>Listen</button>
17
+ <p id="status" aria-live="polite"></p>
16
18
  </main>
17
19
  <script type="module" src="./tool.js"></script>
18
20
  </body>
@@ -11,11 +11,28 @@ $("prompt").textContent = tool.task?.prompt ?? "";
11
11
  // the first of the learner's locales this tool has (manifest.locales), else its first
12
12
  tool.ready(pickLocale(tool.start, manifest.locales));
13
13
 
14
+ // say what happened: a button that does nothing looks broken
15
+ const status = (text) => { $("status").textContent = text; };
16
+
14
17
  $("say").onclick = () => {
15
- if (!tool.task) return;
18
+ // a launch with no task (practice) has nothing to answer
19
+ if (!tool.task) return status("No task to answer: in a lesson, Narai starts one. Practice still works.");
16
20
  // what the learner did, in the payload the manifest declares for this task's kind: never whether it's right
17
21
  tool.answer({ schema: manifest.answers[tool.task.kind], text: $("text").value });
22
+ status("Answer sent: Narai judges it.");
18
23
  };
19
24
  $("drill").onclick = () => {
20
25
  tool.practice("drill", { duration_s: 30, attempts: 1, payload: { schema: manifest.practice.drill, rounds: 1 } });
26
+ status("Practice sent.");
21
27
  };
28
+
29
+ // The microphone is the step's to grant, launch by launch (tool.start.granted), and only when the manifest declares it
30
+ // (capabilities.microphone: true). Without the grant the frame can't open it: show no control for it.
31
+ if (tool.start.granted?.includes("microphone")) {
32
+ $("mic").hidden = false;
33
+ $("mic").onclick = async () => {
34
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
35
+ // listen here, then report what was played (an answer payload such as music.played@1), never whether it's right
36
+ stream.getTracks().forEach((t) => t.stop());
37
+ };
38
+ }