@ishlabs/cli 0.19.0 → 0.20.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.
@@ -164,6 +164,11 @@ export async function resolveSourceRef(client, value, opts) {
164
164
  }
165
165
  // --- Agentic generation jobs ---
166
166
  const GENERATION_POLL_INTERVAL_MS = 2_500;
167
+ /** NEW-CP-3: emit a "still running" heartbeat at least this often even if
168
+ * status/progress_message hasn't changed. Backend often stays at
169
+ * `running` with no message for tens of seconds; without a breadcrumb
170
+ * the CLI looks frozen. */
171
+ const GENERATION_HEARTBEAT_MS = 15_000;
167
172
  const GENERATION_TERMINAL_STATUSES = new Set(["completed", "failed"]);
168
173
  /**
169
174
  * Thrown when a generation job doesn't reach a terminal status before the
@@ -190,7 +195,9 @@ export class GenerationTimeoutError extends Error {
190
195
  export async function pollGenerationJobUntilDone(client, jobId, opts = {}) {
191
196
  const timeoutMs = opts.timeoutMs ?? 600_000;
192
197
  const deadline = Date.now() + timeoutMs;
198
+ const startedAt = Date.now();
193
199
  let lastReported = "";
200
+ let lastHeartbeatAt = Date.now();
194
201
  while (true) {
195
202
  const job = await client.get(`/people/generation-jobs/${jobId}`, undefined, { timeout: 60_000 });
196
203
  if (!opts.quiet) {
@@ -198,8 +205,19 @@ export async function pollGenerationJobUntilDone(client, jobId, opts = {}) {
198
205
  ? `${job.status}: ${job.progress_message}`
199
206
  : job.status;
200
207
  if (line !== lastReported) {
208
+ // Status / message changed — emit it as a fresh breadcrumb.
201
209
  process.stderr.write(` ${line}\n`);
202
210
  lastReported = line;
211
+ lastHeartbeatAt = Date.now();
212
+ }
213
+ else if (Date.now() - lastHeartbeatAt >= GENERATION_HEARTBEAT_MS) {
214
+ // NEW-CP-3: the backend often holds at `status=running` with no
215
+ // `progress_message` for tens of seconds. Without a heartbeat the
216
+ // CLI looks frozen. Emit a "still running" line every 15s so the
217
+ // operator knows we're polling and not hung.
218
+ const elapsedSec = Math.round((Date.now() - startedAt) / 1000);
219
+ process.stderr.write(` still ${job.status} (${elapsedSec}s elapsed)…\n`);
220
+ lastHeartbeatAt = Date.now();
203
221
  }
204
222
  }
205
223
  if (GENERATION_TERMINAL_STATUSES.has(job.status))
@@ -220,6 +220,10 @@ When in doubt: side-by-side comparison usually beats in-place edits. Ids are che
220
220
  - **401 surfaces as fake blocker**: an unauthenticated endpoint produces "participant got stuck on auth screen" — looks like a UX blocker but is config. Always confirm endpoint auth before reading transcripts as user-research data.
221
221
  - **No per-page/per-timestamp scoping for media**: there's no "evaluate just slide 14" or "react to seconds 0-30" API. State the focus explicitly in the \`assignment\` text, or pre-stitch the artifact (e.g. replace one slide locally, upload as a new iteration).
222
222
  - **\`study get --json\` participants live at the top level**, not nested under \`iterations[*].participants\`. The backend split made \`/studies/{id}\` lite (metadata + iteration shells, no participant graph) and added \`/studies/{id}/participants\`; the CLI joins them so \`study get --json\` carries a flat \`participants[]\` with \`iteration_id\` on each row. Read \`.participants[]\`, not \`.iterations[].participants[]\`.
223
+ - **All destructive deletes require \`--yes\` in non-TTY mode**: \`ish workspace delete\`, \`study delete\`, \`ask delete\`, \`person delete\`, \`source delete\`, \`chat endpoint delete\`. In \`--json\` mode (or any piped/non-TTY invocation), omitting \`--yes\` refuses with \`error_kind: "ConfirmationRequired"\` + an \`example\` field showing the same command with \`--yes\` appended. \`workspace delete\` is the highest-blast-radius: it removes ALL nested studies, asks, people, secrets, configs, sources, and chat endpoints — the prompt names them explicitly.
224
+ - **\`ish login\` is idempotent**: with a valid saved token, \`ish login\` short-circuits with "Already logged in" and **does not open a new browser tab**. Use \`--force\` (or \`-f\`) only when actually switching accounts.
225
+ - **\`ish person create\` accepts inline flags** (mirrors \`person update\`): the file-only API (\`--file <path>\`) is preserved as an escape hatch but the common path is \`ish person create --name "X" --type ai --country US ...\` — \`--type\` defaults to \`ai\` when \`--file\` is omitted. See \`ish person create --help\` for the full inline-flag set including \`--household\` (MECE rule applies) and \`--accessibility-profile\`.
226
+ - **\`ish status\` now surfaces \`chat_endpoint\`** alongside \`workspace\`/\`study\`/\`ask\`. Stale or orphan active refs get a \`warning\` + \`hint\` field on the affected ref (instead of silently dropping the \`name\`). On \`workspace use <other>\`, the CLI cascade-clears \`study\`/\`ask\`/\`chat_endpoint\` (they belong to the previous workspace).
223
227
 
224
228
  ## When in doubt
225
229
 
@@ -376,9 +380,13 @@ ish person suggest-scenarios \\
376
380
  # [{"text":"...","source":"situation","scenario_prompt":"..."}, ...]
377
381
  # Valid source values: situation, voice, binary, micro-story
378
382
 
379
- # 3. Save the person shell
383
+ # 3. Save the person shell — either from file:
380
384
  ish person create --file ./persona.json
381
385
  # → p-d4e
386
+ #
387
+ # …or inline (mirror of person update):
388
+ # ish person create --name "Alice" --type ai --country US \\
389
+ # --occupation founder --household single --bio "..."
382
390
 
383
391
  # 4. Persist the answers as structured evidence
384
392
  ish person evidence add p-d4e --traces-file ./answers.json
@@ -1042,7 +1050,7 @@ ish <command> --help
1042
1050
  | \`mcp\` | Wire the hosted ish MCP server into local AI | guides/mcp-add |
1043
1051
  | | clients (Cursor, VS Code, Claude Code, | |
1044
1052
  | | Claude Desktop, Windsurf). Idempotent. | |
1045
- | \`login\` | Browser-based auth | — |
1053
+ | \`login\` | Browser-based auth. Idempotent: short-circuits on valid saved token. \`--force\` to switch accounts. | — |
1046
1054
  | \`logout\` | Clear saved credentials | — |
1047
1055
  | \`status\` | Show active session (user, workspace, | concepts/active-context |
1048
1056
  | | study, ask, token validity) — alias \`whoami\` | |
package/dist/upgrade.js CHANGED
@@ -32,11 +32,18 @@ export async function upgrade(currentVersion, targetVersion) {
32
32
  const scriptUrl = import.meta.url;
33
33
  const runningFromNodeModules = scriptUrl.includes("/node_modules/");
34
34
  if (looksLikeNode || runningFromNodeModules) {
35
- throw new Error("Cannot self-upgrade an npm-installed CLI (would overwrite the node binary). " +
35
+ // Pattern C: tag as ValidationError so exitCodeFromError maps to 2
36
+ // (usage_error). Otherwise this would fall through to the generic
37
+ // exit 1 even with the wrapper in place (ISSUE-012).
38
+ const err = new Error("Cannot self-upgrade an npm-installed CLI (would overwrite the node binary). " +
36
39
  "Run `npm install -g @ishlabs/cli@latest` instead.");
40
+ err.name = "ValidationError";
41
+ throw err;
37
42
  }
38
43
  if (targetVersion && !/^\d+\.\d+\.\d+/.test(targetVersion)) {
39
- throw new Error(`Invalid version format: ${targetVersion}`);
44
+ const err = new Error(`Invalid version format: ${targetVersion}`);
45
+ err.name = "ValidationError";
46
+ throw err;
40
47
  }
41
48
  const latest = targetVersion || (await getLatestVersion());
42
49
  if (latest === currentVersion) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ishlabs/cli",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "description": "The command-line interface for ish",
5
5
  "type": "module",
6
6
  "bin": {