@hireobi/cli 0.1.9 → 0.1.10

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
@@ -12,6 +12,8 @@ npm i -g @hireobi/cli && obi
12
12
 
13
13
  Windows PowerShell 5.1 has no `&&`: run `npm i -g @hireobi/cli`, then `obi`.
14
14
 
15
+ To update, run the same line again: `obi` refreshes the Skill links. At most once a day, any command asks npm for the newest version; when yours is older, a JSON envelope carries `update` and a human run prints one line on stderr, and the Skill offers to update. Offline or slow, the check stays silent. `obi doctor` shows the installed and newest versions. `OBI_NO_UPDATE_CHECK=1` turns the check off, and so does `CI`. A version the service no longer accepts fails with `CLI_UPDATE_REQUIRED` and the update command.
16
+
15
17
  The first `obi` registers the Obi Skill for Claude Code, Codex, Cursor and OpenCode, and every later command keeps it registered: two personal links, `~/.claude/skills/obi` and `~/.agents/skills/obi`. A conflicting file or link is never replaced: the command still runs, and bare `obi` says what is in the way and how to fix it. `obi skill install` registers it again after a fix and removes the links an earlier alpha of this package made under its former name; `obi skill path` locates the bundled Skill. `OBI_SKILL_LINKS=off` leaves the links to `obi skill install`. A cloud harness needs its own supported installation path.
16
18
 
17
19
  Run by a coding agent, bare `obi` prints where the Skill's guide is and the next command. In an interactive terminal it shows the Obi mascot and a short welcome, then exits: the conversation happens in the assistant. The greeting is English or French; `OBI_LANGUAGE` overrides `LC_ALL`, `LC_MESSAGES`, then `LANG`.
package/dist/obi.js CHANGED
@@ -16664,7 +16664,43 @@ var SUPPORTED_MARKET_NAMES = SUPPORTED_MARKETS.map(
16664
16664
  (market) => market.name
16665
16665
  );
16666
16666
 
16667
+ // ../shared/lib/cli-version.ts
16668
+ var CLI_VERSION_HEADER = "x-obi-cli-version";
16669
+ var CLI_UPDATE_COMMAND = "npm i -g @hireobi/cli";
16670
+ var release = /^(0|[1-9][0-9]{0,8})\.(0|[1-9][0-9]{0,8})\.(0|[1-9][0-9]{0,8})$/;
16671
+ function parseRelease(version3) {
16672
+ const match = release.exec(version3);
16673
+ return match === null ? null : [Number(match[1]), Number(match[2]), Number(match[3])];
16674
+ }
16675
+ function isOlderRelease(version3, other) {
16676
+ const left = parseRelease(version3);
16677
+ const right = parseRelease(other);
16678
+ if (left === null || right === null) return false;
16679
+ for (let index = 0; index < 3; index += 1) {
16680
+ if (left[index] !== right[index]) return left[index] < right[index];
16681
+ }
16682
+ return false;
16683
+ }
16684
+
16685
+ // src/version.ts
16686
+ var cliVersion = "0.1.10";
16687
+
16667
16688
  // src/product-api-failures.ts
16689
+ var versionHeaders = { [CLI_VERSION_HEADER]: cliVersion };
16690
+ function updateRequiredError() {
16691
+ return new CliError({
16692
+ code: "CLI_UPDATE_REQUIRED",
16693
+ message: `This Obi CLI (${cliVersion}) is too old for the Obi service.`,
16694
+ exitCode: 7,
16695
+ retryable: false,
16696
+ nextAction: `Run ${CLI_UPDATE_COMMAND}, then run obi once to refresh the Skill, and try again.`,
16697
+ mode: "product"
16698
+ });
16699
+ }
16700
+ function apiFailure(code, httpStatus, retryAfterSeconds) {
16701
+ if (code === "CLI_UPDATE_REQUIRED") throw updateRequiredError();
16702
+ return { ok: false, failure: { code, httpStatus, retryAfterSeconds } };
16703
+ }
16668
16704
  function transportError(code, message, exitCode, retryable) {
16669
16705
  return new CliError({
16670
16706
  code,
@@ -17294,7 +17330,9 @@ var pendingAuthorizationViewSchema = external_exports.object({
17294
17330
  var originMarker = {
17295
17331
  origin: external_exports.string().min(1).max(200).optional(),
17296
17332
  development: external_exports.literal(true).optional(),
17297
- evaluationCaseId: external_exports.uuid().optional()
17333
+ evaluationCaseId: external_exports.uuid().optional(),
17334
+ // Set when npm holds a newer CLI than this one (cli/src/update-check.ts).
17335
+ update: external_exports.object({ installed: external_exports.string().max(32), latest: external_exports.string().max(32), command: external_exports.string().max(80) }).optional()
17298
17336
  };
17299
17337
  var helpDataSchema = external_exports.object({
17300
17338
  usage: external_exports.string(),
@@ -17339,6 +17377,10 @@ var capabilitiesDataSchema = external_exports.object({
17339
17377
  var localDoctorDataSchema = external_exports.object({
17340
17378
  node: external_exports.string(),
17341
17379
  cliVersion: external_exports.string(),
17380
+ latestVersion: external_exports.string().nullable(),
17381
+ // Set only when npm could not be reached: the last version it named.
17382
+ lastKnownVersion: external_exports.string().nullable(),
17383
+ updateCheck: external_exports.enum(["on", "off"]),
17342
17384
  origin: external_exports.string(),
17343
17385
  credentialStorage: external_exports.union([backendKindSchema, external_exports.literal("none")]),
17344
17386
  videoTools: external_exports.object({ ytDlp: external_exports.boolean(), ffmpeg: external_exports.boolean() }),
@@ -17780,7 +17822,7 @@ function retryAfter(response) {
17780
17822
  return typeof value === "string" && /^[0-9]{1,5}$/.test(value) ? Number(value) : null;
17781
17823
  }
17782
17824
  async function send(context, request) {
17783
- const headers2 = { accept: "application/json" };
17825
+ const headers2 = { accept: "application/json", ...versionHeaders };
17784
17826
  if (request.body !== void 0) {
17785
17827
  headers2["content-type"] = "application/json";
17786
17828
  }
@@ -17990,12 +18032,7 @@ function interpret(response, schema) {
17990
18032
  return { ok: true, data: parsed.data.data };
17991
18033
  }
17992
18034
  const failure2 = ProductApiErrorResponseSchema.safeParse(body);
17993
- if (failure2.success) {
17994
- return {
17995
- ok: false,
17996
- failure: { code: failure2.data.error.code, httpStatus: response.status, retryAfterSeconds: retryAfter(response) }
17997
- };
17998
- }
18035
+ if (failure2.success) return apiFailure(failure2.data.error.code, response.status, retryAfter(response));
17999
18036
  if (response.status >= 500) {
18000
18037
  throw transportError("SERVICE_UNAVAILABLE", "The Obi API is unavailable.", 6, true);
18001
18038
  }
@@ -20376,7 +20413,7 @@ function memoryText(data) {
20376
20413
 
20377
20414
  // src/support-commands.ts
20378
20415
  import { createHash as createHash2 } from "node:crypto";
20379
- import { release } from "node:os";
20416
+ import { release as release2 } from "node:os";
20380
20417
 
20381
20418
  // src/last-error.ts
20382
20419
  import { readFile as readFile6, writeFile as writeFile3 } from "node:fs/promises";
@@ -20400,9 +20437,6 @@ async function readLastError(stateDirectory) {
20400
20437
  }
20401
20438
  }
20402
20439
 
20403
- // src/version.ts
20404
- var cliVersion = "0.1.9";
20405
-
20406
20440
  // src/support-commands.ts
20407
20441
  async function probeServer(origin, timeoutMs) {
20408
20442
  try {
@@ -20419,7 +20453,7 @@ async function probeServer(origin, timeoutMs) {
20419
20453
  }
20420
20454
  async function sendSupport(context, input) {
20421
20455
  const lastError = await readLastError(context.stateDirectory);
20422
- const diagnostic = { cliVersion, node: process.version, os: `${process.platform} ${release()} ${process.arch}`, lastError };
20456
+ const diagnostic = { cliVersion, node: process.version, os: `${process.platform} ${release2()} ${process.arch}`, lastError };
20423
20457
  let account2 = null;
20424
20458
  let reason = null;
20425
20459
  try {
@@ -20695,7 +20729,9 @@ Next: ${sanitizeHumanText(result.data.nextStep)}
20695
20729
  degraded: `Obi server degraded: try again later, and write to ${SUPPORT_EMAIL} if it lasts.`,
20696
20730
  unreachable: `Obi server unreachable: check the network connection, and write to ${SUPPORT_EMAIL} if it lasts.`
20697
20731
  }[result.data.server];
20698
- return `Local diagnostic completed for ${sanitizeHumanText(result.data.origin)}: ${storage}; ${video}. ${server}
20732
+ const { cliVersion: installed, latestVersion: latest } = result.data;
20733
+ const version3 = result.data.updateCheck === "off" ? `CLI ${sanitizeHumanText(installed)} (update check off)` : latest === null ? `CLI ${sanitizeHumanText(installed)} (latest version unknown: npm could not be reached${result.data.lastKnownVersion === null ? "" : `, last known ${sanitizeHumanText(result.data.lastKnownVersion)}`})` : isOlderRelease(installed, latest) ? `CLI ${sanitizeHumanText(installed)}, latest ${sanitizeHumanText(latest)}: update with ${CLI_UPDATE_COMMAND}` : `CLI ${sanitizeHumanText(installed)}, the latest`;
20734
+ return `Local diagnostic completed for ${sanitizeHumanText(result.data.origin)}: ${version3}; ${storage}; ${video}. ${server}
20699
20735
  `;
20700
20736
  }
20701
20737
  if (result.command === "login") {
@@ -21240,7 +21276,7 @@ function accountMismatch(account2, revoked, expected) {
21240
21276
  }
21241
21277
  async function confirmAndStore(context, store, pair, receivedAt, expectedEmail) {
21242
21278
  const api = context;
21243
- const release3 = guardNewFamily(api, pair);
21279
+ const release4 = guardNewFamily(api, pair);
21244
21280
  let stored = false;
21245
21281
  try {
21246
21282
  const status = await readAccountStatus(api, pair.accessToken);
@@ -21271,7 +21307,7 @@ async function confirmAndStore(context, store, pair, receivedAt, expectedEmail)
21271
21307
  }
21272
21308
  await store.writeCredential(credentialFromPair(context.origin.origin, pair, confirmed, receivedAt));
21273
21309
  stored = true;
21274
- release3();
21310
+ release4();
21275
21311
  });
21276
21312
  try {
21277
21313
  await store.removePending();
@@ -21291,7 +21327,7 @@ async function confirmAndStore(context, store, pair, receivedAt, expectedEmail)
21291
21327
  }
21292
21328
  throw error51;
21293
21329
  } finally {
21294
- release3();
21330
+ release4();
21295
21331
  }
21296
21332
  }
21297
21333
  async function login(session, options) {
@@ -21606,7 +21642,7 @@ async function safeFetch(url3, opts = {}) {
21606
21642
  if (!REDIRECT_STATUSES.has(res.status)) return res;
21607
21643
  const location = res.headers.get("location");
21608
21644
  if (!location) return res;
21609
- await release2(res);
21645
+ await release3(res);
21610
21646
  let next;
21611
21647
  try {
21612
21648
  next = new URL(location, current).href;
@@ -21617,7 +21653,7 @@ async function safeFetch(url3, opts = {}) {
21617
21653
  }
21618
21654
  throw new TooManyRedirectsError(url3, maxRedirects);
21619
21655
  }
21620
- async function release2(res) {
21656
+ async function release3(res) {
21621
21657
  await res.body?.cancel().catch((err) => {
21622
21658
  console.warn("[safe-fetch] cancelling an unread body failed:", err);
21623
21659
  });
@@ -21625,7 +21661,7 @@ async function release2(res) {
21625
21661
  async function readCapped(res, maxBytes = MAX_RESPONSE_BYTES) {
21626
21662
  const declared = Number(res.headers.get("content-length"));
21627
21663
  if (Number.isFinite(declared) && declared > maxBytes) {
21628
- await release2(res);
21664
+ await release3(res);
21629
21665
  return "";
21630
21666
  }
21631
21667
  if (!res.body) return (await res.text()).slice(0, maxBytes);
@@ -33601,9 +33637,13 @@ function statusResult(status) {
33601
33637
  }
33602
33638
  async function localDoctor(runtime, timeoutMs) {
33603
33639
  const located = await locateCredential(runtime);
33640
+ const updates = await runtime.updates;
33604
33641
  return success2("doctor", "local", {
33605
33642
  node: process.version,
33606
33643
  cliVersion,
33644
+ latestVersion: updates?.state === "on" && updates.reached ? updates.latest : null,
33645
+ lastKnownVersion: updates?.state === "on" && !updates.reached ? updates.latest : null,
33646
+ updateCheck: updates?.state ?? "off",
33607
33647
  origin: runtime.origin.origin,
33608
33648
  credentialStorage: located === null ? "none" : located.store.backend.kind,
33609
33649
  videoTools: await videoTools(),
@@ -33818,6 +33858,81 @@ function renderStart(input) {
33818
33858
  ].join("\n") + "\n";
33819
33859
  }
33820
33860
 
33861
+ // src/update-check.ts
33862
+ import { readFile as readFile9, writeFile as writeFile4 } from "node:fs/promises";
33863
+ import { join as join8 } from "node:path";
33864
+ var UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
33865
+ var UPDATE_CHECK_TIMEOUT_MS = 1500;
33866
+ var releaseSchema = external_exports.string().max(32).refine((value) => parseRelease(value) !== null);
33867
+ var cacheSchema = external_exports.object({
33868
+ checkedAt: external_exports.iso.datetime(),
33869
+ latest: releaseSchema.nullable()
33870
+ });
33871
+ var cacheFile = (stateDirectory) => join8(stateDirectory, "update-check.json");
33872
+ var npmRegistry = "https://registry.npmjs.org";
33873
+ var loopbackRegistry = /^http:\/\/(127\.0\.0\.1|\[::1\]):[1-9][0-9]{0,4}$/;
33874
+ function updateCheckDisabled(env) {
33875
+ const on = (value) => value !== void 0 && value !== "" && value !== "0" && value.toLowerCase() !== "false";
33876
+ return on(env.OBI_NO_UPDATE_CHECK) || on(env.CI);
33877
+ }
33878
+ async function fetchLatestVersion(env, timeoutMs) {
33879
+ const override = env.OBI_UPDATE_REGISTRY;
33880
+ const registry2 = override !== void 0 && loopbackRegistry.test(override) ? override : npmRegistry;
33881
+ try {
33882
+ const response = await exchange({
33883
+ url: new URL("/-/package/@hireobi%2fcli/dist-tags", registry2),
33884
+ method: "GET",
33885
+ headers: { accept: "application/json" },
33886
+ timeoutMs,
33887
+ responseLimitBytes: 16 * 1024
33888
+ });
33889
+ if (response.status !== 200) return null;
33890
+ const parsed = external_exports.object({ latest: releaseSchema }).safeParse(JSON.parse(response.body));
33891
+ return parsed.success ? parsed.data.latest : null;
33892
+ } catch {
33893
+ return null;
33894
+ }
33895
+ }
33896
+ async function readCache(stateDirectory) {
33897
+ try {
33898
+ const parsed = cacheSchema.safeParse(JSON.parse(await readFile9(cacheFile(stateDirectory), "utf8")));
33899
+ return parsed.success ? parsed.data : null;
33900
+ } catch {
33901
+ return null;
33902
+ }
33903
+ }
33904
+ async function writeCache(stateDirectory, cache) {
33905
+ try {
33906
+ await ensureStateDirectory(stateDirectory);
33907
+ await writeFile4(cacheFile(stateDirectory), JSON.stringify(cache), { mode: 384 });
33908
+ } catch {
33909
+ }
33910
+ }
33911
+ async function checkForUpdate(input) {
33912
+ if (updateCheckDisabled(input.env) || parseRelease(input.installed) === null) return { state: "off" };
33913
+ try {
33914
+ const now = (input.now ?? Date.now)();
33915
+ const cache = await readCache(input.stateDirectory);
33916
+ const age = cache === null ? Infinity : now - Date.parse(cache.checkedAt);
33917
+ let latest = cache?.latest ?? null;
33918
+ let reached = true;
33919
+ if (input.fresh === true || !(age >= 0 && age < UPDATE_CHECK_INTERVAL_MS)) {
33920
+ const fetched = await (input.fetchLatest ?? ((timeoutMs) => fetchLatestVersion(input.env, timeoutMs)))(UPDATE_CHECK_TIMEOUT_MS);
33921
+ reached = fetched !== null;
33922
+ latest = fetched ?? latest;
33923
+ await writeCache(input.stateDirectory, { checkedAt: new Date(now).toISOString(), latest });
33924
+ }
33925
+ const notice = latest !== null && isOlderRelease(input.installed, latest) ? { installed: input.installed, latest, command: CLI_UPDATE_COMMAND } : null;
33926
+ return { state: "on", latest, reached, notice };
33927
+ } catch {
33928
+ return { state: "on", latest: null, reached: false, notice: null };
33929
+ }
33930
+ }
33931
+ function updateNoticeLine(notice) {
33932
+ return `Obi CLI ${notice.latest} is available (you have ${notice.installed}). Update with: ${notice.command}
33933
+ `;
33934
+ }
33935
+
33821
33936
  // src/welcome.ts
33822
33937
  var copy = {
33823
33938
  en: {
@@ -33941,6 +34056,17 @@ function failureResult(error51, command, mode) {
33941
34056
  function markOrigin(result, origin) {
33942
34057
  return origin?.development === true ? cliResultSchema.parse({ ...result, origin: origin.origin, development: true }) : result;
33943
34058
  }
34059
+ async function updateNotice(updates) {
34060
+ const check3 = await updates;
34061
+ return check3?.state === "on" ? check3.notice : null;
34062
+ }
34063
+ function withUpdate(result, notice) {
34064
+ return notice === null ? result : cliResultSchema.parse({ ...result, update: notice });
34065
+ }
34066
+ async function announceUpdate(updates) {
34067
+ const notice = await updateNotice(updates);
34068
+ if (notice !== null) process.stderr.write(updateNoticeLine(notice));
34069
+ }
33944
34070
  async function runCli() {
33945
34071
  const argv = process.argv.slice(2);
33946
34072
  const json2 = wantsJson(argv);
@@ -33953,6 +34079,7 @@ async function runCli() {
33953
34079
  argv.some((value) => value === "--test-server" || value.startsWith("--test-server="))
33954
34080
  );
33955
34081
  let origin;
34082
+ let updates;
33956
34083
  let warned = false;
33957
34084
  const activeOrigin = () => {
33958
34085
  const resolved = resolveOrigin(process.env);
@@ -33968,7 +34095,9 @@ async function runCli() {
33968
34095
  command = args.command;
33969
34096
  mode = commandMode(args.command, args.testServer !== void 0);
33970
34097
  origin = activeOrigin();
33971
- const runtime = { origin, stateDirectory: await prepareStateDirectory(process.env) };
34098
+ const stateDirectory = await prepareStateDirectory(process.env);
34099
+ updates = checkForUpdate({ env: process.env, stateDirectory, installed: cliVersion, fresh: command === "doctor" });
34100
+ const runtime = { origin, stateDirectory, updates };
33972
34101
  if (start) {
33973
34102
  const located = await locateCredential(runtime, "skip");
33974
34103
  process.stdout.write(renderStart({
@@ -33976,6 +34105,7 @@ async function runCli() {
33976
34105
  skillDirectory: packagedSkillDirectory(),
33977
34106
  accountEmail: located === null ? null : located.credential.account.email
33978
34107
  }));
34108
+ await announceUpdate(updates);
33979
34109
  return;
33980
34110
  }
33981
34111
  const result = await executeCommand(args, runtime);
@@ -33991,8 +34121,10 @@ async function runCli() {
33991
34121
  }) : result;
33992
34122
  if (scoped.ok && welcome) {
33993
34123
  process.stdout.write(renderWelcome(process.env, process.stdout.columns, process.stdout.getColorDepth(), registration));
34124
+ await announceUpdate(updates);
33994
34125
  } else {
33995
- writeResult(markOrigin(scoped, origin), args.json);
34126
+ writeResult(withUpdate(markOrigin(scoped, origin), args.json ? await updateNotice(updates) : null), args.json);
34127
+ if (!args.json) await announceUpdate(updates);
33996
34128
  }
33997
34129
  } catch (error51) {
33998
34130
  if (origin === void 0) {
@@ -34015,7 +34147,8 @@ async function runCli() {
34015
34147
  { code: knownError.code, command: failure2.command }
34016
34148
  ), () => void 0);
34017
34149
  }
34018
- writeResult(markOrigin(failure2, origin), json2);
34150
+ writeResult(withUpdate(markOrigin(failure2, origin), json2 ? await updateNotice(updates) : null), json2);
34151
+ if (!json2) await announceUpdate(updates);
34019
34152
  process.exitCode = knownError.exitCode;
34020
34153
  } finally {
34021
34154
  closeIdleConnections();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hireobi/cli",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "description": "Obi, the AI Search Employee: CLI and Skill for your AI assistant",
5
5
  "keywords": [
6
6
  "obi",
@@ -53,6 +53,8 @@ Lead with the outcome, material uncertainty and one useful next action. On an in
53
53
 
54
54
  When a failure cannot be recovered in this conversation (the same error after the retry its `nextAction` names, or `obi doctor --json` reporting `server` as `degraded` or `unreachable`), say so and offer once to send it to Obi support. On the customer's yes, run `obi support "<what happened, in their words>" --json`: it adds the CLI version, the system and the last error code itself. When it fails with `SUPPORT_NOT_SENT`, nothing was sent: give them support@hireobi.com and the `support.diagnosticText` to paste. Never put a token or a credential file in the message.
55
55
 
56
+ When a CLI result carries `update` (or stderr says a newer Obi CLI is available), tell the customer in one sentence, once per conversation, and offer to update it for them. Run `update.command` (`npm i -g @hireobi/cli`) only on their yes, then run `obi` once so the Skill links refresh, and ask them to open a new chat if this one keeps the old Skill. Without a yes, carry on with the current version. `CLI_UPDATE_REQUIRED` means the service refuses this version: say so, offer the same update, and retry the command after it.
57
+
56
58
  Separate site observations, customer confirmations, host inferences and independently measured answers. Request only the private extracts needed for the agreed scope; keep secrets and unrelated workspace material local. A local draft or patch is a reviewable suggestion until the customer accepts it. The Skill runs when invoked; while the harness is closed, saved server measurements can continue but host research and reasoning wait for resumption.
57
59
 
58
60
  Stay in customer mode even inside the Obi source repository. A product-use failure is explained and recovered in this conversation. Open a development task, branch or pull request only after an explicit separate software-development request.