@junoflow/cli 0.15.0 → 0.17.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/verbs.json CHANGED
@@ -1,8 +1,33 @@
1
1
  {
2
+ "groups": {
3
+ "state": "States: named values, most decided by a layer stack of rules, claims and bindings",
4
+ "flow": "Flows: TypeScript automations started by events, schedules, webhooks or calls; authored as a working copy",
5
+ "run": "A flow's runs: what each one did and how it ended",
6
+ "timer": "Upcoming timers: every flow schedule, soonest first",
7
+ "activity": "The run timeline across every flow",
8
+ "integration": "Integrations: installed TypeScript that talks to an outside service; authored as a working copy",
9
+ "device": "Devices: follow states and write them to one endpoint (a Home Assistant entity, an MQTT topic)",
10
+ "control": "Controls: buttons, switches and selects a person presses",
11
+ "app": "Apps: small web pages for people, backed by Juno; authored as a working copy",
12
+ "finding": "Findings: standing facts flows, integrations and tasks assert (\"freezer too warm\")",
13
+ "webhook": "Webhooks: URLs that start flows",
14
+ "task": "Tasks: jobs that report to a ping URL, chores a person is reminded of, work a flow opens",
15
+ "series": "Series: values over time an integration publishes (a forecast, a price list)",
16
+ "credential": "Credentials: the secrets flows and integrations use, by id; only the user sets a value",
17
+ "person": "People and their contacts: who Juno notifies and asks",
18
+ "question": "Questions flows ask people, and the answers",
19
+ "approval": "Destructive commands waiting for a person to approve them in Juno",
20
+ "ha": "Home Assistant: entities, their history, services",
21
+ "mqtt": "MQTT: the topics seen and the broker connection",
22
+ "script": "Run an exploratory TypeScript script in Juno's sandbox",
23
+ "session": "Conversations with Juno's own assistant (juno ask)",
24
+ "config": "This machine's CLI settings: the instance URL and the API key"
25
+ },
2
26
  "commands": [
3
27
  {
4
28
  "command": "discover",
5
29
  "tool": "discover",
30
+ "summary": "Search everything in Juno by what the user calls it; every hit carries a kind and an id",
6
31
  "id_field": "query",
7
32
  "flags": {
8
33
  "kind": { "field": "kinds", "type": "string", "multiple": true }
@@ -12,6 +37,7 @@
12
37
  {
13
38
  "command": "related",
14
39
  "tool": "get_related",
40
+ "summary": "Walk the reference graph from one thing: what it uses and what uses it",
15
41
  "positional": "kind:id",
16
42
  "flags": { "degree": { "field": "degree", "type": "integer" } },
17
43
  "naming_exception": "takes any kind, so it has no group"
@@ -27,6 +53,11 @@
27
53
  "tool": "get_state_layers",
28
54
  "id_field": "id"
29
55
  },
56
+ {
57
+ "command": "state list-layer-versions",
58
+ "tool": "list_state_layer_versions",
59
+ "id_field": "id"
60
+ },
30
61
  { "command": "state evaluate-level", "tool": "evaluate_level" },
31
62
  {
32
63
  "command": "state preview-layers",
@@ -43,6 +74,12 @@
43
74
  "id_field": "id",
44
75
  "fixed_input": { "dry_run": false }
45
76
  },
77
+ {
78
+ "command": "state restore-layers",
79
+ "tool": "restore_state_layers",
80
+ "id_field": "id",
81
+ "flags": { "version": { "field": "version", "type": "integer" } }
82
+ },
46
83
  {
47
84
  "command": "state set-value",
48
85
  "tool": "set_state_value",
@@ -50,12 +87,39 @@
50
87
  },
51
88
  { "command": "state claim", "tool": "claim_state", "id_field": "id" },
52
89
  { "command": "state release", "tool": "release_state", "id_field": "id" },
90
+ { "command": "state delete", "tool": "delete_state", "id_field": "id" },
53
91
  { "command": "flow get", "tool": "get_flow", "id_field": "id" },
92
+ {
93
+ "command": "flow list-versions",
94
+ "tool": "list_flow_versions",
95
+ "id_field": "id"
96
+ },
97
+ {
98
+ "command": "flow get-next-fire",
99
+ "tool": "get_next_fire",
100
+ "id_field": "flow_id",
101
+ "flags": { "trigger": { "field": "trigger", "type": "string" } }
102
+ },
103
+ {
104
+ "command": "flow restore-version",
105
+ "tool": "restore_flow_version",
106
+ "id_field": "id",
107
+ "flags": { "version": { "field": "version", "type": "integer" } }
108
+ },
54
109
  {
55
110
  "command": "flow set-enabled",
56
111
  "tool": "set_flow_enabled",
57
112
  "id_field": "flow_id"
58
113
  },
114
+ {
115
+ "command": "flow rename",
116
+ "tool": "rename_flow",
117
+ "id_field": "id",
118
+ "flags": {
119
+ "name": { "field": "name", "type": "string" },
120
+ "slug": { "field": "slug", "type": "string" }
121
+ }
122
+ },
59
123
  {
60
124
  "command": "flow trigger",
61
125
  "tool": "trigger_flow",
@@ -66,6 +130,7 @@
66
130
  "tool": "fire_trigger",
67
131
  "id_field": "flow_id"
68
132
  },
133
+ { "command": "flow delete", "tool": "delete_flow", "id_field": "id" },
69
134
  {
70
135
  "command": "flow init",
71
136
  "effect": "read",
@@ -104,22 +169,93 @@
104
169
  },
105
170
  { "command": "run list", "tool": "list_runs" },
106
171
  { "command": "run get", "tool": "get_run", "id_field": "run_id" },
172
+ { "command": "timer list", "tool": "list_timers" },
173
+ {
174
+ "command": "activity list",
175
+ "tool": "list_activity",
176
+ "flags": {
177
+ "hours": { "field": "hours", "type": "integer" },
178
+ "flow": { "field": "flow_id", "type": "string" },
179
+ "limit": { "field": "limit", "type": "integer" }
180
+ }
181
+ },
107
182
  {
108
183
  "command": "integration get",
109
184
  "tool": "get_integration",
110
185
  "id_field": "id"
111
186
  },
187
+ {
188
+ "command": "integration list-versions",
189
+ "tool": "list_integration_versions",
190
+ "id_field": "id"
191
+ },
192
+ { "command": "integration preview", "tool": "preview_integration" },
193
+ {
194
+ "command": "integration list-invocations",
195
+ "tool": "list_integration_invocations",
196
+ "id_field": "id",
197
+ "flags": { "limit": { "field": "limit", "type": "integer" } }
198
+ },
199
+ {
200
+ "command": "integration get-deliveries",
201
+ "tool": "get_integration_deliveries",
202
+ "id_field": "id",
203
+ "flags": { "limit": { "field": "limit", "type": "integer" } }
204
+ },
205
+ {
206
+ "command": "integration get-usage",
207
+ "tool": "get_integration_usage",
208
+ "id_field": "id"
209
+ },
210
+ { "command": "integration list-registry", "tool": "list_registry" },
112
211
  {
113
212
  "command": "integration configure",
114
213
  "tool": "configure_integration",
115
214
  "id_field": "id"
116
215
  },
216
+ {
217
+ "command": "integration restore-version",
218
+ "tool": "restore_integration_version",
219
+ "id_field": "id",
220
+ "flags": { "version": { "field": "version", "type": "integer" } }
221
+ },
222
+ {
223
+ "command": "integration set-enabled",
224
+ "tool": "set_integration_enabled",
225
+ "id_field": "id"
226
+ },
227
+ {
228
+ "command": "integration bind-webhook",
229
+ "tool": "bind_integration_webhook",
230
+ "id_field": "id",
231
+ "flags": {
232
+ "slot": { "field": "slot", "type": "string" },
233
+ "webhook": { "field": "webhook", "type": "string" }
234
+ }
235
+ },
117
236
  { "command": "integration install", "tool": "install_integration" },
237
+ {
238
+ "command": "integration invoke-action",
239
+ "tool": "invoke_integration_action",
240
+ "id_field": "id",
241
+ "flags": { "action": { "field": "action", "type": "string" } }
242
+ },
118
243
  {
119
244
  "command": "integration run-schedule",
120
245
  "tool": "run_integration_schedule",
121
246
  "id_field": "integration"
122
247
  },
248
+ {
249
+ "command": "integration upgrade",
250
+ "tool": "upgrade_integration",
251
+ "id_field": "id",
252
+ "flags": { "ref": { "field": "ref", "type": "string" } }
253
+ },
254
+ {
255
+ "command": "integration uninstall",
256
+ "tool": "uninstall_integration",
257
+ "id_field": "id"
258
+ },
123
259
  {
124
260
  "command": "integration init",
125
261
  "effect": "read",
@@ -158,17 +294,59 @@
158
294
  },
159
295
  { "command": "device list", "tool": "list_devices" },
160
296
  { "command": "device get", "tool": "get_device", "id_field": "id" },
297
+ {
298
+ "command": "device list-versions",
299
+ "tool": "list_device_versions",
300
+ "id_field": "id"
301
+ },
161
302
  { "command": "device preview", "tool": "preview_device" },
162
303
  { "command": "device write", "tool": "write_device" },
304
+ {
305
+ "command": "device restore-version",
306
+ "tool": "restore_device_version",
307
+ "id_field": "id",
308
+ "flags": { "version": { "field": "version", "type": "integer" } }
309
+ },
163
310
  { "command": "device suspend", "tool": "suspend_device", "id_field": "id" },
164
311
  { "command": "device release", "tool": "release_device", "id_field": "id" },
312
+ { "command": "device delete", "tool": "delete_device", "id_field": "id" },
313
+ { "command": "control get", "tool": "get_control", "id_field": "id" },
314
+ { "command": "control list", "tool": "list_controls" },
315
+ {
316
+ "command": "control get-history",
317
+ "tool": "get_control_history",
318
+ "id_field": "id"
319
+ },
320
+ {
321
+ "command": "control get-activity",
322
+ "tool": "get_control_activity",
323
+ "id_field": "id",
324
+ "flags": { "limit": { "field": "limit", "type": "integer" } }
325
+ },
326
+ {
327
+ "command": "control list-versions",
328
+ "tool": "list_control_versions",
329
+ "id_field": "id"
330
+ },
165
331
  { "command": "control write", "tool": "write_control" },
332
+ {
333
+ "command": "control restore-version",
334
+ "tool": "restore_control_version",
335
+ "id_field": "id",
336
+ "flags": { "version": { "field": "version", "type": "integer" } }
337
+ },
166
338
  { "command": "control use", "tool": "use_control", "id_field": "id" },
339
+ {
340
+ "command": "control extend",
341
+ "tool": "extend_control",
342
+ "id_field": "id"
343
+ },
167
344
  {
168
345
  "command": "control release",
169
346
  "tool": "release_control",
170
347
  "id_field": "id"
171
348
  },
349
+ { "command": "control delete", "tool": "delete_control", "id_field": "id" },
172
350
  { "command": "app get", "tool": "get_app", "id_field": "id" },
173
351
  {
174
352
  "command": "app list-invocations",
@@ -176,6 +354,39 @@
176
354
  "id_field": "id",
177
355
  "flags": { "limit": { "field": "limit", "type": "integer" } }
178
356
  },
357
+ {
358
+ "command": "app list-versions",
359
+ "tool": "list_app_versions",
360
+ "id_field": "id"
361
+ },
362
+ { "command": "app list-links", "tool": "list_app_links", "id_field": "id" },
363
+ {
364
+ "command": "app restore-version",
365
+ "tool": "restore_app_version",
366
+ "id_field": "id",
367
+ "flags": { "version": { "field": "version", "type": "integer" } }
368
+ },
369
+ {
370
+ "command": "app set-enabled",
371
+ "tool": "set_app_enabled",
372
+ "id_field": "id"
373
+ },
374
+ {
375
+ "command": "app create-link",
376
+ "tool": "create_app_link",
377
+ "id_field": "id",
378
+ "flags": {
379
+ "name": { "field": "name", "type": "string" },
380
+ "expires-at": { "field": "expires_at", "type": "string" }
381
+ }
382
+ },
383
+ {
384
+ "command": "app revoke-link",
385
+ "tool": "revoke_app_link",
386
+ "id_field": "id",
387
+ "flags": { "link": { "field": "link", "type": "string" } }
388
+ },
389
+ { "command": "app delete", "tool": "delete_app", "id_field": "id" },
179
390
  {
180
391
  "command": "app init",
181
392
  "effect": "read",
@@ -246,16 +457,125 @@
246
457
  },
247
458
  { "command": "webhook write", "tool": "write_webhook", "id_field": "id" },
248
459
  { "command": "webhook fire", "tool": "fire_webhook", "id_field": "id" },
460
+ { "command": "webhook delete", "tool": "delete_webhook", "id_field": "id" },
461
+ {
462
+ "command": "webhook rotate-token",
463
+ "tool": "rotate_webhook_token",
464
+ "id_field": "id"
465
+ },
249
466
  { "command": "task list", "tool": "list_tasks" },
250
467
  { "command": "task get-runs", "tool": "get_task_runs", "id_field": "id" },
468
+ {
469
+ "command": "task get-occurrence",
470
+ "tool": "get_task_occurrence",
471
+ "id_field": "id",
472
+ "flags": { "key": { "field": "key", "type": "string" } }
473
+ },
474
+ {
475
+ "command": "task inbox",
476
+ "tool": "list_task_inbox",
477
+ "naming_exception": "the inbox's To do list is what a person calls it; list-inbox reads as a list of inboxes"
478
+ },
251
479
  { "command": "task write", "tool": "write_task", "id_field": "id" },
252
480
  { "command": "task delete", "tool": "delete_task", "id_field": "id" },
253
- { "command": "task command", "tool": "command_task", "id_field": "id" },
481
+ {
482
+ "command": "task rotate-token",
483
+ "tool": "rotate_task_token",
484
+ "id_field": "id"
485
+ },
486
+ {
487
+ "command": "task open",
488
+ "tool": "command_task",
489
+ "id_field": "id",
490
+ "flags": {
491
+ "key": { "field": "key", "type": "string" },
492
+ "at": { "field": "at", "type": "string" },
493
+ "within": { "field": "within", "type": "string" },
494
+ "reason": { "field": "reason", "type": "string" }
495
+ },
496
+ "fixed_input": { "verb": "open" },
497
+ "summary": "Open a piece of work, due at --at (now) and overdue --within after",
498
+ "naming_exception": "one command per verb of command_task, which the row fixes"
499
+ },
500
+ {
501
+ "command": "task snooze",
502
+ "tool": "command_task",
503
+ "id_field": "id",
504
+ "flags": {
505
+ "key": { "field": "key", "type": "string" },
506
+ "until": { "field": "until", "type": "string" }
507
+ },
508
+ "fixed_input": { "verb": "snooze" },
509
+ "summary": "Move a piece of work's due date later, to --until",
510
+ "naming_exception": "one command per verb of command_task, which the row fixes"
511
+ },
512
+ {
513
+ "command": "task set-due",
514
+ "tool": "command_task",
515
+ "id_field": "id",
516
+ "flags": {
517
+ "key": { "field": "key", "type": "string" },
518
+ "at": { "field": "at", "type": "string" }
519
+ },
520
+ "fixed_input": { "verb": "set-due" },
521
+ "summary": "Set a piece of work's due date, earlier or later, to --at",
522
+ "naming_exception": "one command per verb of command_task, which the row fixes"
523
+ },
524
+ {
525
+ "command": "task complete",
526
+ "tool": "command_task",
527
+ "id_field": "id",
528
+ "flags": {
529
+ "key": { "field": "key", "type": "string" },
530
+ "outcome": { "field": "outcome", "type": "string" },
531
+ "note": { "field": "note", "type": "string" },
532
+ "check": { "field": "check", "type": "integer", "multiple": true }
533
+ },
534
+ "fixed_input": { "verb": "complete" },
535
+ "summary": "Mark a piece of work done; --check ticks checklist items by number",
536
+ "naming_exception": "one command per verb of command_task, which the row fixes"
537
+ },
538
+ {
539
+ "command": "task skip",
540
+ "tool": "command_task",
541
+ "id_field": "id",
542
+ "flags": {
543
+ "key": { "field": "key", "type": "string" },
544
+ "note": { "field": "note", "type": "string" }
545
+ },
546
+ "fixed_input": { "verb": "skip" },
547
+ "summary": "Close a manual task's work as not being done, opening its skipped finding",
548
+ "naming_exception": "one command per verb of command_task, which the row fixes"
549
+ },
550
+ {
551
+ "command": "task cancel",
552
+ "tool": "command_task",
553
+ "id_field": "id",
554
+ "flags": {
555
+ "key": { "field": "key", "type": "string" },
556
+ "reason": { "field": "reason", "type": "string" }
557
+ },
558
+ "fixed_input": { "verb": "cancel" },
559
+ "summary": "Close a piece of work that is no longer needed, with no run and no finding",
560
+ "naming_exception": "one command per verb of command_task, which the row fixes"
561
+ },
254
562
  { "command": "series get", "tool": "get_series", "id_field": "id" },
255
563
  { "command": "credential list", "tool": "list_credentials" },
564
+ {
565
+ "command": "credential delete",
566
+ "tool": "delete_credential",
567
+ "id_field": "id"
568
+ },
256
569
  { "command": "person list", "tool": "list_people" },
257
570
  { "command": "person get", "tool": "get_person", "id_field": "id" },
258
571
  { "command": "person write", "tool": "write_person", "id_field": "id" },
572
+ {
573
+ "command": "person test-contact",
574
+ "tool": "test_contact",
575
+ "id_field": "id",
576
+ "flags": { "contact": { "field": "contact", "type": "string" } }
577
+ },
578
+ { "command": "person delete", "tool": "delete_person", "id_field": "id" },
259
579
  {
260
580
  "command": "question list",
261
581
  "tool": "list_questions",
@@ -277,6 +597,12 @@
277
597
  "for": { "field": "for", "type": "string" }
278
598
  }
279
599
  },
600
+ {
601
+ "command": "approval list",
602
+ "tool": "list_approvals",
603
+ "flags": { "status": { "field": "status", "type": "string" } }
604
+ },
605
+ { "command": "approval get", "tool": "get_approval", "id_field": "id" },
280
606
  {
281
607
  "command": "ha get-entity",
282
608
  "tool": "get_entity",
@@ -288,7 +614,26 @@
288
614
  "id_field": "entity_id"
289
615
  },
290
616
  { "command": "ha list-services", "tool": "list_services" },
617
+ {
618
+ "command": "ha list-entities",
619
+ "tool": "list_entities",
620
+ "flags": {
621
+ "query": { "field": "query", "type": "string" },
622
+ "limit": { "field": "limit", "type": "integer" }
623
+ }
624
+ },
625
+ { "command": "ha get-diagnostics", "tool": "get_ha_diagnostics" },
291
626
  { "command": "ha call-service", "tool": "call_service" },
627
+ {
628
+ "command": "mqtt list-topics",
629
+ "tool": "list_topics",
630
+ "flags": {
631
+ "query": { "field": "query", "type": "string" },
632
+ "sort": { "field": "sort", "type": "string" },
633
+ "limit": { "field": "limit", "type": "integer" }
634
+ }
635
+ },
636
+ { "command": "mqtt get-diagnostics", "tool": "get_mqtt_diagnostics" },
292
637
  {
293
638
  "command": "script run",
294
639
  "tool": "run_script",
@@ -307,11 +652,56 @@
307
652
  "effect": "act",
308
653
  "summary": "Prompt Juno's in-app assistant"
309
654
  },
655
+ {
656
+ "command": "whoami",
657
+ "effect": "read",
658
+ "summary": "Print who the instance takes you for: the principal, its scope and the linked person"
659
+ },
660
+ {
661
+ "command": "version",
662
+ "effect": "read",
663
+ "summary": "Print the instance's version, build time and schema version, and this CLI's version"
664
+ },
665
+ {
666
+ "command": "permissions",
667
+ "effect": "read",
668
+ "summary": "Print Claude Code permission rules that allow the read (and optionally change) commands"
669
+ },
670
+ {
671
+ "command": "session list",
672
+ "effect": "read",
673
+ "summary": "List assistant sessions, newest first"
674
+ },
675
+ {
676
+ "command": "session get",
677
+ "effect": "read",
678
+ "summary": "Print an assistant session: its status, what it waits on, and with --transcript its transcript"
679
+ },
680
+ {
681
+ "command": "session export",
682
+ "effect": "read",
683
+ "summary": "Print an assistant session's full export, transcript and tool calls included"
684
+ },
685
+ {
686
+ "command": "session rate",
687
+ "effect": "change",
688
+ "summary": "Rate an assistant session: up, down, addressed (a thumbs down whose cause is fixed) or none"
689
+ },
310
690
  {
311
691
  "command": "session approve",
312
692
  "effect": "act",
313
693
  "summary": "Approve the tool call an assistant session is waiting on"
314
694
  },
695
+ {
696
+ "command": "session cancel",
697
+ "effect": "act",
698
+ "summary": "Cancel an assistant session that is running or waiting"
699
+ },
700
+ {
701
+ "command": "approval wait",
702
+ "effect": "read",
703
+ "summary": "Wait for a person to decide an approval, then print how it ended"
704
+ },
315
705
  {
316
706
  "command": "task run",
317
707
  "effect": "act",
@@ -331,11 +721,6 @@
331
721
  "command": "config set",
332
722
  "effect": "local",
333
723
  "summary": "Remember the instance URL or the API key in the user config file"
334
- },
335
- {
336
- "command": "agent-skill install",
337
- "effect": "local",
338
- "summary": "Write Juno's skill for a coding agent and print its permission block"
339
724
  }
340
725
  ]
341
726
  }
@@ -1,64 +0,0 @@
1
- import { mkdir, writeFile } from 'node:fs/promises';
2
- import { join, resolve } from 'node:path';
3
-
4
- import { connect } from '../api.js';
5
- import { UsageError } from '../errors.js';
6
- import { SKILL_DIR } from '../installed-skill.js';
7
-
8
- export const AGENT_SKILL_INSTALL_USAGE =
9
- 'juno agent-skill install [--dir <path>] [--allow-change] [--working-copy] [--agents-md] [--json]';
10
-
11
- /**
12
- * `juno agent-skill install` writes the skill GET /api/agent-skill renders to
13
- * `<dir>/SKILL.md` (default `.claude/skills/juno` under the current
14
- * directory) and prints a Claude Code permission block on stdout: every read
15
- * command, plus every change command with --allow-change, plus the working
16
- * copy's npm scripts of the same tiers with --working-copy. No act command is
17
- * ever in it — the server leaves them out, and the CLI adds nothing.
18
- *
19
- * `--agents-md` prints the same text as an AGENTS.md section instead and
20
- * writes nothing; `--json` prints the route's body.
21
- */
22
- export async function agentSkillInstall(args, io) {
23
- if (args._.length > 0) {
24
- throw new UsageError(
25
- `juno agent-skill install takes no argument; usage: ${AGENT_SKILL_INSTALL_USAGE}`
26
- );
27
- }
28
- const { client } = await connect(args, io);
29
- const skill = await client.getAgentSkill();
30
-
31
- if (args.json) return printJSON(skill, io);
32
- if (args['agents-md']) {
33
- io.write(skill.agents_md);
34
- return 0;
35
- }
36
-
37
- const dir = resolve(io.cwd, args.dir ?? SKILL_DIR);
38
- const path = join(dir, 'SKILL.md');
39
- await mkdir(dir, { recursive: true });
40
- await writeFile(path, skill.skill_md, 'utf8');
41
-
42
- const tiers = (t) => [
43
- ...(t?.read ?? []),
44
- ...(args['allow-change'] ? (t?.change ?? []) : []),
45
- ];
46
- const allow = tiers(skill.permissions);
47
- if (args['working-copy'])
48
- allow.push(...tiers(skill.working_copy_permissions));
49
-
50
- io.warn(`wrote ${path} (version ${skill.version})`);
51
- io.warn(
52
- args['working-copy']
53
- ? "Add these rules to this working copy's .claude/settings.json — not to user-wide settings, " +
54
- "where they would also allow any other project's npm scripts:"
55
- : 'Add these rules to .claude/settings.json (or ~/.claude/settings.json) to run them without a prompt:'
56
- );
57
- return printJSON({ permissions: { allow } }, io);
58
- }
59
-
60
- /** Compact off a TTY, indented on one, as every JSON the CLI prints. */
61
- function printJSON(value, io) {
62
- io.log(io.isTTY ? JSON.stringify(value, null, 2) : JSON.stringify(value));
63
- return 0;
64
- }
@@ -1,55 +0,0 @@
1
- import { readFile } from 'node:fs/promises';
2
- import { homedir } from 'node:os';
3
- import { dirname, join, resolve } from 'node:path';
4
-
5
- /** Where `juno agent-skill install` writes the skill, relative to a root. */
6
- export const SKILL_DIR = join('.claude', 'skills', 'juno');
7
-
8
- const VERSION_LINE = /^\s*(?:<!--\s*)?juno-skill-version:\s*"?([^"\s]+)"?/m;
9
-
10
- /**
11
- * installedSkill finds the Juno skill a harness would load: SKILL.md under
12
- * `.claude/skills/juno` in the current directory or the nearest parent that
13
- * has one, then in the home directory. It answers the first file found and the
14
- * version it records (null when it records none), or null when there is no
15
- * file. A file that cannot be read counts as absent: the check it serves is a
16
- * courtesy, never a failure.
17
- */
18
- export async function installedSkill(cwd, env = process.env) {
19
- const candidates = [];
20
- let dir = resolve(cwd);
21
- for (;;) {
22
- candidates.push(join(dir, SKILL_DIR, 'SKILL.md'));
23
- const parent = dirname(dir);
24
- if (parent === dir) break;
25
- dir = parent;
26
- }
27
- candidates.push(join(env.HOME || homedir(), SKILL_DIR, 'SKILL.md'));
28
-
29
- for (const path of candidates) {
30
- let text;
31
- try {
32
- text = await readFile(path, 'utf8');
33
- } catch {
34
- continue;
35
- }
36
- return { path, version: VERSION_LINE.exec(text)?.[1] ?? null };
37
- }
38
- return null;
39
- }
40
-
41
- /**
42
- * warnIfStale says on stderr when the installed skill records a version other
43
- * than the one the instance serves (`agent_skill_version` on GET /api/skills).
44
- * Silent when either side has none.
45
- */
46
- export async function warnIfStale(index, target, io) {
47
- const served = index?.agent_skill_version;
48
- if (!served) return;
49
- const installed = await installedSkill(io.cwd, io.env);
50
- if (!installed?.version || installed.version === served) return;
51
- io.warn(
52
- `the installed Juno skill (${installed.path}) is version ${installed.version}; ` +
53
- `${target} serves ${served} — run \`juno agent-skill install\` to update it`
54
- );
55
- }