octwin-cli 0.6.1 → 0.7.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.
Files changed (3) hide show
  1. package/CHANGELOG.md +540 -520
  2. package/dist/index.js +118 -0
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -620,6 +620,54 @@ async function notifyIfKbStale(flags) {
620
620
  }
621
621
  catch { /* a KB check must never break the CLI */ }
622
622
  }
623
+ /**
624
+ * Nudge (to stderr) when the platform has memos this workspace has not read — a reply to
625
+ * a `octwin feedback` report, or a notice we published (new capability, deprecation,
626
+ * breaking change).
627
+ *
628
+ * The sibling of `notifyIfKbStale`, and the same three decisions apply for the same
629
+ * reasons: it rides commands that ALREADY hit the platform so it costs one tiny GET on
630
+ * top of networked work; it never throws, because observing must not break a command;
631
+ * and it is **deliberately not TTY-gated**, because the primary reader of this CLI is an
632
+ * authoring agent whose output is piped — the one reader that cannot notice an unread
633
+ * reply on its own would otherwise be the only one never told.
634
+ *
635
+ * WHY THIS EXISTS AT ALL. `octwin feedback` was one-way. Measured 2026-08-18: four
636
+ * reports sat unread for nine days, and one author hardcoded English across 16 flows to
637
+ * work around a bug that had been fixed two days earlier, because nothing could tell
638
+ * them. A nudge that does not name the command to run does not motivate an agent, so the
639
+ * line always ends in `octwin memos`.
640
+ *
641
+ * Unlike the KB check there is NO local state to compare: read state is per-tenant on the
642
+ * platform, so an agent on a fresh machine or in a fresh container still learns about an
643
+ * unread memo. That is the whole reason it is not a local marker file.
644
+ */
645
+ async function notifyIfMemosWaiting(flags) {
646
+ try {
647
+ const t = resolveTargetOrNull(flags);
648
+ if (!t)
649
+ return;
650
+ const { status, json } = await apiGet(`${t.url}/api/self/p/memos?meta=1`, t);
651
+ // Silent on ANY failure — an older platform has no such route, and a nudge is never
652
+ // worth a diagnostic of its own. Unlike the KB poll there is no scope to explain: the
653
+ // route is gated on tenant access precisely so every token can answer it.
654
+ if (status !== 200 || !json || typeof json !== 'object')
655
+ return;
656
+ const unread = Number(json.unread ?? 0);
657
+ if (!Number.isFinite(unread) || unread <= 0)
658
+ return;
659
+ const actionable = Number(json.unread_actionable ?? 0);
660
+ const what = unread === 1 ? '1 memo' : `${unread} memos`;
661
+ const tail = actionable > 0 ? ` (${actionable} needing action)` : '';
662
+ console.error(`\n✉ ${what} from the platform${tail} — read them: octwin memos`);
663
+ if (actionable > 0) {
664
+ // Said separately, because the whole point of the severity axis is that an agent
665
+ // mid-build should stop and read rather than finish first.
666
+ console.error(' One or more may change what you are building — read before continuing.');
667
+ }
668
+ }
669
+ catch { /* a memo check must never break the CLI */ }
670
+ }
623
671
  /** Which commands already made a platform call, so the trailing KB-drift poll
624
672
  * rides on existing network work (never on offline `validate` / `init`;
625
673
  * `platform-kb` refreshes the reference itself, so it needs no nudge). */
@@ -632,6 +680,9 @@ function commandTouchesPlatform(command, flags) {
632
680
  case 'chat':
633
681
  case 'media':
634
682
  case 'pull':
683
+ // `memos` is networked but needs NO memo nudge — it just read them. It still gets the
684
+ // KB drift check, which is a different question.
685
+ case 'memos': return true;
635
686
  case 'records':
636
687
  case 'work':
637
688
  case 'logs':
@@ -2058,6 +2109,60 @@ async function cmdFeedback(flags) {
2058
2109
  console.log(' ⓘ no local capability reference found, so the report carries no KB version.');
2059
2110
  console.log(' Pull it before your next session: octwin platform-kb');
2060
2111
  }
2112
+ console.log(' A reply arrives as a memo — this CLI will tell you when one is waiting.');
2113
+ }
2114
+ /**
2115
+ * `octwin memos [--json] [--all]` — read what the platform has told you.
2116
+ *
2117
+ * The other half of `octwin feedback`. Two kinds arrive here: a REPLY to a report you
2118
+ * sent, and a NOTICE we published to every author (a new capability, a deprecation, a
2119
+ * breaking change). Both are markdown, printed in full — this is a channel for reading,
2120
+ * not a list to page through, so there is no `show <id>`.
2121
+ *
2122
+ * Reading ACKS: the default prints unread memos and marks them read, so the nudge stops.
2123
+ * `--all` re-reads history and acks nothing, which is also the escape hatch if a piped
2124
+ * `--json` consumer crashed halfway through.
2125
+ */
2126
+ async function cmdMemos(flags) {
2127
+ const t = resolveTarget(flags);
2128
+ const all = flags.all === true;
2129
+ const { status, json } = await apiGet(`${t.url}/api/self/p/memos${all ? '?all=1' : ''}`, t);
2130
+ if (status !== 200) {
2131
+ die(`could not read memos (HTTP ${status})${errDetail(json)}${authFailureDetail(status, t.url)}`);
2132
+ }
2133
+ const rows = (json?.rows ?? []);
2134
+ if (flags.json === true) {
2135
+ console.log(JSON.stringify(rows, null, 2));
2136
+ return;
2137
+ }
2138
+ if (rows.length === 0) {
2139
+ console.log(all ? 'No memos yet.' : 'No unread memos. (History: octwin memos --all)');
2140
+ return;
2141
+ }
2142
+ const mark = (s) => s === 'breaking' ? '⛔ BREAKING' : s === 'action_required' ? '⚠ ACTION' : 'ⓘ';
2143
+ for (const m of rows) {
2144
+ const when = String(m.published_at ?? '').slice(0, 10);
2145
+ const about = m.pack_id ? ` · ${m.pack_id}` : '';
2146
+ const seen = m.read_at ? ' · (already read)' : '';
2147
+ console.log(`\n${'─'.repeat(72)}`);
2148
+ console.log(`${mark(m.severity)} ${m.title}`);
2149
+ console.log(`${m.kind === 'reply' ? 'reply to your report' : 'platform notice'} · ${when}${about}${seen}`);
2150
+ console.log(`${'─'.repeat(72)}\n`);
2151
+ console.log(m.body_md.trimEnd());
2152
+ }
2153
+ console.log();
2154
+ // Ack only what we just showed as unread, and only on the default path — `--all`
2155
+ // deliberately leaves read state alone so history can be re-read any number of times.
2156
+ if (!all) {
2157
+ const ids = rows.filter(m => !m.read_at).map(m => m.id);
2158
+ if (ids.length > 0) {
2159
+ const res = await apiSend('POST', `${t.url}/api/self/p/memos/read`, { ids }, t);
2160
+ // A failed ack is not a failed command — the author HAS read them. Say so rather
2161
+ // than dying after printing everything, and the nudge simply fires again.
2162
+ if (res.status !== 200)
2163
+ console.error(`ⓘ could not mark them read (HTTP ${res.status}) — they will be offered again.`);
2164
+ }
2165
+ }
2061
2166
  }
2062
2167
  /** `octwin logs [conversationId] [--as <handle>] [--json]` — list conversations
2063
2168
  * or show one's event timeline (full text + the renders each turn produced). */
@@ -3751,6 +3856,7 @@ function help() {
3751
3856
  octwin platform-kb [pull] [--if-stale|--check] [--dir .] [--url <url>] # no token needed
3752
3857
  octwin test [--dir .] # = validate --remote (the full platform check)
3753
3858
  octwin feedback [--dir .] # submit this pack's FEEDBACK.md to the platform team
3859
+ octwin memos [--all] [--json] # read the platform's replies + notices (a reply to your feedback lands here)
3754
3860
 
3755
3861
  Writes — exercise the state your pack creates (each needs the matching :write scope):
3756
3862
  octwin records create <entity> --set field=value … # also: patch <id> --entity <e>, stage <id> --to <s>, note <id> "…"
@@ -3980,6 +4086,13 @@ octwin projects rm <slug> [--yes]
3980
4086
  agent loops that want to branch without parsing prose.`,
3981
4087
  test: `octwin test [--dir .]
3982
4088
  Alias for \`octwin validate --remote\` — the full platform check.`,
4089
+ memos: `octwin memos [--all] [--json]
4090
+ Read what the platform has told you: a REPLY to a report you sent with
4091
+ \`octwin feedback\`, or a NOTICE published to every author (a new capability,
4092
+ a deprecation, a breaking change). Bodies are printed in full.
4093
+ Reading marks them read, so the reminder stops. --all re-reads history and
4094
+ acks nothing. --json to branch on \`severity\`
4095
+ (info | action_required | breaking).`,
3983
4096
  feedback: `octwin feedback [--dir .]
3984
4097
  Submit this pack's FEEDBACK.md to the platform team.
3985
4098
  The octwin-pack skill writes that file in its last step — findings grouped by
@@ -4070,6 +4183,9 @@ async function main() {
4070
4183
  case 'feedback':
4071
4184
  await cmdFeedback(flags);
4072
4185
  break;
4186
+ case 'memos':
4187
+ await cmdMemos(flags);
4188
+ break;
4073
4189
  case 'test':
4074
4190
  await cmdValidate({ ...flags, remote: true });
4075
4191
  break; // A6: `test` = the full remote validate, not a validate-clone
@@ -4091,6 +4207,8 @@ async function main() {
4091
4207
  // offline paths); CLI-upgrade always.
4092
4208
  if (commandTouchesPlatform(command, flags))
4093
4209
  await notifyIfKbStale(flags);
4210
+ if (commandTouchesPlatform(command, flags))
4211
+ await notifyIfMemosWaiting(flags);
4094
4212
  await notifyIfOutdated();
4095
4213
  }
4096
4214
  main().catch((err) => die(err?.message ?? String(err)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "octwin-cli",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "Octwin external-pack developer CLI (by CEQUENS) — scaffold, validate, deploy, and check pure-YAML packs on your tenant.",
5
5
  "type": "module",
6
6
  "bin": {