jeopi-cli 16.4.4 → 16.5.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.
@@ -73,33 +73,44 @@ interface UiString extends UiBase {
73
73
  export type AnyUiMetadata = UiBase & {
74
74
  options?: ReadonlyArray<SubmenuOption> | "runtime";
75
75
  };
76
- interface BooleanDef {
76
+ /**
77
+ * Marks a setting whose value is a credential.
78
+ *
79
+ * Lives at the top level rather than inside `ui` so it can also describe a
80
+ * setting the settings panel never shows and therefore cannot carry
81
+ * `ui.secret`. Read it through `isCredential`, which is the single accessor
82
+ * both the CLI and the settings panel consult.
83
+ */
84
+ interface CredentialMarker {
85
+ credential?: true;
86
+ }
87
+ interface BooleanDef extends CredentialMarker {
77
88
  type: "boolean";
78
89
  default: boolean | undefined;
79
90
  ui?: UiBoolean;
80
91
  }
81
- interface StringDef {
92
+ interface StringDef extends CredentialMarker {
82
93
  type: "string";
83
94
  default: string | undefined;
84
95
  ui?: UiString;
85
96
  }
86
- interface NumberDef {
97
+ interface NumberDef extends CredentialMarker {
87
98
  type: "number";
88
99
  default: number | undefined;
89
100
  ui?: UiNumber;
90
101
  }
91
- interface EnumDef<T extends readonly string[]> {
102
+ interface EnumDef<T extends readonly string[]> extends CredentialMarker {
92
103
  type: "enum";
93
104
  values: T;
94
105
  default: T[number];
95
106
  ui?: UiEnum<T>;
96
107
  }
97
- interface ArrayDef<T> {
108
+ interface ArrayDef<T> extends CredentialMarker {
98
109
  type: "array";
99
110
  default: T[];
100
111
  ui?: UiBase;
101
112
  }
102
- interface RecordDef<T> {
113
+ interface RecordDef<T> extends CredentialMarker {
103
114
  type: "record";
104
115
  default: Record<string, T>;
105
116
  ui?: UiBase;
@@ -127,6 +138,7 @@ export declare const SETTINGS_SCHEMA: {
127
138
  readonly "auth.broker.token": {
128
139
  readonly type: "string";
129
140
  readonly default: undefined;
141
+ readonly credential: true;
130
142
  };
131
143
  readonly autoResume: {
132
144
  readonly type: "boolean";
@@ -5336,6 +5348,7 @@ export declare const SETTINGS_SCHEMA: {
5336
5348
  readonly "searxng.token": {
5337
5349
  readonly type: "string";
5338
5350
  readonly default: undefined;
5351
+ readonly credential: true;
5339
5352
  };
5340
5353
  readonly "searxng.basicUsername": {
5341
5354
  readonly type: "string";
@@ -5344,6 +5357,7 @@ export declare const SETTINGS_SCHEMA: {
5344
5357
  readonly "searxng.basicPassword": {
5345
5358
  readonly type: "string";
5346
5359
  readonly default: undefined;
5360
+ readonly credential: true;
5347
5361
  };
5348
5362
  readonly "searxng.categories": {
5349
5363
  readonly type: "string";
@@ -5400,6 +5414,7 @@ export declare const SETTINGS_SCHEMA: {
5400
5414
  readonly "dev.autoqaPush.token": {
5401
5415
  readonly type: "string";
5402
5416
  readonly default: undefined;
5417
+ readonly credential: true;
5403
5418
  };
5404
5419
  /**
5405
5420
  * User decision on sharing automatic `report_tool_issue` grievances.
@@ -5494,6 +5509,12 @@ export declare function getDefault<P extends SettingPath>(path: P): SettingValue
5494
5509
  export declare function hasUi(path: SettingPath): boolean;
5495
5510
  /** Get UI metadata for a path (undefined if no UI) */
5496
5511
  export declare function getUi(path: SettingPath): AnyUiMetadata | undefined;
5512
+ /**
5513
+ * True when a setting holds a credential and must never be printed in a bulk
5514
+ * dump. The single accessor every surface consults, so the CLI and the settings
5515
+ * panel cannot diverge on what counts as a secret.
5516
+ */
5517
+ export declare function isCredential(path: SettingPath): boolean;
5497
5518
  /** Get all paths for a specific tab */
5498
5519
  export declare function getPathsForTab(tab: SettingTab): SettingPath[];
5499
5520
  /** Get the type of a setting */
@@ -34,7 +34,16 @@ export declare class BlobStore {
34
34
  }
35
35
  /** Check if a data string is a blob reference. */
36
36
  export declare function isBlobRef(data: string): boolean;
37
- /** Extract the SHA-256 hash from a blob reference string. */
37
+ /**
38
+ * Extract the SHA-256 hash from a blob reference string.
39
+ *
40
+ * Returns null when the string is not a blob ref, or when the suffix is not a
41
+ * canonical 64-char lowercase hex hash. Rejecting non-hash suffixes here is the
42
+ * single choke point that keeps every resolution path confined to the blob dir:
43
+ * `get`/`getSync`/`has` feed this value into `path.join(this.dir, hash)`, so an
44
+ * unvalidated `../` suffix in a crafted session file would otherwise escape the
45
+ * store and read arbitrary files.
46
+ */
38
47
  export declare function parseBlobRef(data: string): string | null;
39
48
  /** Identify provider transport image data URLs so persistence can externalize and restore them losslessly. */
40
49
  export declare function isImageDataUrl(data: string): boolean;
@@ -1,3 +1,4 @@
1
+ import type { AssistantMessage } from "jeopi-ai";
1
2
  import type { SessionEntry } from "./session-entries";
2
3
  export declare const TOOL_EXECUTION_START_CUSTOM_TYPE = "tool_execution_start";
3
4
  export declare const SESSION_EXIT_CUSTOM_TYPE = "session_exit";
@@ -35,6 +36,16 @@ export interface SessionExitData {
35
36
  recordedAt: string;
36
37
  pendingToolCalls?: PendingToolCallDiagnostic[];
37
38
  }
39
+ export interface AssistantModelMetadata {
40
+ api: AssistantMessage["api"];
41
+ provider: string;
42
+ model: string;
43
+ }
44
+ /**
45
+ * createInterruptedTurnAbortMessage returns a terminal assistant record when
46
+ * the latest persisted process exit follows a non-terminal conversation tail.
47
+ */
48
+ export declare function createInterruptedTurnAbortMessage(entries: readonly SessionEntry[], fallbackModel?: AssistantModelMetadata): AssistantMessage | undefined;
38
49
  /**
39
50
  * Project full tool-call arguments down to the fields the pending-tool-call
40
51
  * resume warning actually renders (`command`/`path`), truncated. Returns
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "jeopi-cli",
4
- "version": "16.4.4",
4
+ "version": "16.5.0",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/akillness/jeopi",
7
7
  "author": "Can Boluk",
@@ -57,17 +57,17 @@
57
57
  "@agentclientprotocol/sdk": "0.25.0",
58
58
  "@babel/parser": "^7.29.7",
59
59
  "@mozilla/readability": "^0.6.0",
60
- "jeopi-hashline": "16.4.4",
61
- "jeopi-stats": "16.4.4",
62
- "jeopi-agent-core": "16.4.4",
63
- "jeopi-ai": "16.4.4",
64
- "jeopi-catalog": "16.4.4",
65
- "jeopi-mnemopi": "16.4.4",
66
- "jeopi-natives": "16.4.4",
67
- "jeopi-tui": "16.4.4",
68
- "jeopi-utils": "16.4.4",
69
- "jeopi-wire": "16.4.4",
70
- "jeopi-snapcompact": "16.4.4",
60
+ "jeopi-hashline": "16.5.0",
61
+ "jeopi-stats": "16.5.0",
62
+ "jeopi-agent-core": "16.5.0",
63
+ "jeopi-ai": "16.5.0",
64
+ "jeopi-catalog": "16.5.0",
65
+ "jeopi-mnemopi": "16.5.0",
66
+ "jeopi-natives": "16.5.0",
67
+ "jeopi-tui": "16.5.0",
68
+ "jeopi-utils": "16.5.0",
69
+ "jeopi-wire": "16.5.0",
70
+ "jeopi-snapcompact": "16.5.0",
71
71
  "@opentelemetry/api": "^1.9.1",
72
72
  "@opentelemetry/context-async-hooks": "^2.7.1",
73
73
  "@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
@@ -77,7 +77,7 @@
77
77
  "@puppeteer/browsers": "^3.0.4",
78
78
  "@types/turndown": "5.0.6",
79
79
  "@xterm/headless": "^6.0.0",
80
- "arktype": "^2.2.0",
80
+ "arktype": "2.2.3",
81
81
  "chalk": "^5.6.2",
82
82
  "diff": "^9.0.0",
83
83
  "fast-xml-parser": "^5.9.0",
@@ -86,7 +86,7 @@
86
86
  "lru-cache": "11.5.1",
87
87
  "mammoth": "^1.12.0",
88
88
  "mupdf": "^1.27.0",
89
- "puppeteer-core": "^25.1.0",
89
+ "puppeteer-core": "25.4.0",
90
90
  "turndown": "7.2.4",
91
91
  "turndown-plugin-gfm": "1.0.2",
92
92
  "zod": "^4"
@@ -12,6 +12,7 @@ import {
12
12
  getEnumValues,
13
13
  getType,
14
14
  getUi,
15
+ isCredential,
15
16
  type SettingPath,
16
17
  Settings,
17
18
  type SettingValue,
@@ -50,6 +51,9 @@ type CliSettingDef = {
50
51
 
51
52
  const ALL_SETTING_PATHS = Object.keys(SETTINGS_SCHEMA) as SettingPath[];
52
53
 
54
+ /** Printed instead of a credential value in human output only. */
55
+ const REDACTED = "********";
56
+
53
57
  /** Find setting definition by path */
54
58
  function findSettingDef(path: string): CliSettingDef | undefined {
55
59
  if (!(path in SETTINGS_SCHEMA)) return undefined;
@@ -278,13 +282,20 @@ function handleList(flags: { json?: boolean }): void {
278
282
  const defs = ALL_SETTING_PATHS.map(path => findSettingDef(path)).filter((def): def is CliSettingDef => !!def);
279
283
 
280
284
  if (flags.json) {
281
- const result: Record<string, { value: unknown; type: string; description: string }> = {};
285
+ // A redacted entry omits `value` and says so, rather than substituting a
286
+ // placeholder string: a consumer cannot tell a stand-in from a real value
287
+ // and could write it back as the credential.
288
+ //
289
+ // Redaction is driven by the value, not by classification alone. Marking an
290
+ // unset credential as redacted would report every fresh install as having
291
+ // one configured, which leaks the opposite of what redaction is for.
292
+ const result: Record<string, { value?: unknown; redacted?: true; type: string; description: string }> = {};
282
293
  for (const def of defs) {
283
- result[def.path] = {
284
- value: settings.get(def.path),
285
- type: def.type,
286
- description: def.description,
287
- };
294
+ const value = settings.get(def.path);
295
+ result[def.path] =
296
+ isCredential(def.path) && value
297
+ ? { redacted: true, type: def.type, description: def.description }
298
+ : { value, type: def.type, description: def.description };
288
299
  }
289
300
  console.log(JSON.stringify(result, null, 2));
290
301
  return;
@@ -309,8 +320,13 @@ function handleList(flags: { json?: boolean }): void {
309
320
  for (const group of sortedGroups) {
310
321
  console.log(chalk.bold.blue(`[${group}]`));
311
322
  for (const def of groups[group]) {
323
+ // `list` dumps every value without anyone asking for a specific
324
+ // credential, so redact here. `get <path>` stays an explicit
325
+ // single-value request and is left alone. An unset or cleared ("")
326
+ // credential keeps its ordinary rendering: masking it would imply one
327
+ // is configured.
312
328
  const value = settings.get(def.path);
313
- const valueStr = formatValue(value);
329
+ const valueStr = isCredential(def.path) && value ? REDACTED : formatValue(value);
314
330
  const typeStr = getTypeDisplay(def);
315
331
  console.log(` ${chalk.white(def.path)} = ${valueStr} ${chalk.dim(typeStr)}`);
316
332
  }
@@ -219,38 +219,50 @@ export type AnyUiMetadata = UiBase & {
219
219
  options?: ReadonlyArray<SubmenuOption> | "runtime";
220
220
  };
221
221
 
222
- interface BooleanDef {
222
+ /**
223
+ * Marks a setting whose value is a credential.
224
+ *
225
+ * Lives at the top level rather than inside `ui` so it can also describe a
226
+ * setting the settings panel never shows and therefore cannot carry
227
+ * `ui.secret`. Read it through `isCredential`, which is the single accessor
228
+ * both the CLI and the settings panel consult.
229
+ */
230
+ interface CredentialMarker {
231
+ credential?: true;
232
+ }
233
+
234
+ interface BooleanDef extends CredentialMarker {
223
235
  type: "boolean";
224
236
  default: boolean | undefined;
225
237
  ui?: UiBoolean;
226
238
  }
227
239
 
228
- interface StringDef {
240
+ interface StringDef extends CredentialMarker {
229
241
  type: "string";
230
242
  default: string | undefined;
231
243
  ui?: UiString;
232
244
  }
233
245
 
234
- interface NumberDef {
246
+ interface NumberDef extends CredentialMarker {
235
247
  type: "number";
236
248
  default: number | undefined;
237
249
  ui?: UiNumber;
238
250
  }
239
251
 
240
- interface EnumDef<T extends readonly string[]> {
252
+ interface EnumDef<T extends readonly string[]> extends CredentialMarker {
241
253
  type: "enum";
242
254
  values: T;
243
255
  default: T[number];
244
256
  ui?: UiEnum<T>;
245
257
  }
246
258
 
247
- interface ArrayDef<T> {
259
+ interface ArrayDef<T> extends CredentialMarker {
248
260
  type: "array";
249
261
  default: T[];
250
262
  ui?: UiBase;
251
263
  }
252
264
 
253
- interface RecordDef<T> {
265
+ interface RecordDef<T> extends CredentialMarker {
254
266
  type: "record";
255
267
  default: Record<string, T>;
256
268
  ui?: UiBase;
@@ -344,7 +356,7 @@ export const SETTINGS_SCHEMA = {
344
356
  // Env (`JEOPI_AUTH_BROKER_URL` / `JEOPI_AUTH_BROKER_TOKEN`) takes precedence so
345
357
  // per-machine overrides remain trivial.
346
358
  "auth.broker.url": { type: "string", default: undefined },
347
- "auth.broker.token": { type: "string", default: undefined },
359
+ "auth.broker.token": { type: "string", default: undefined, credential: true },
348
360
 
349
361
  autoResume: {
350
362
  type: "boolean",
@@ -4889,6 +4901,7 @@ export const SETTINGS_SCHEMA = {
4889
4901
  "searxng.token": {
4890
4902
  type: "string",
4891
4903
  default: undefined,
4904
+ credential: true,
4892
4905
  },
4893
4906
 
4894
4907
  "searxng.basicUsername": {
@@ -4899,6 +4912,7 @@ export const SETTINGS_SCHEMA = {
4899
4912
  "searxng.basicPassword": {
4900
4913
  type: "string",
4901
4914
  default: undefined,
4915
+ credential: true,
4902
4916
  },
4903
4917
 
4904
4918
  "searxng.categories": {
@@ -4948,6 +4962,7 @@ export const SETTINGS_SCHEMA = {
4948
4962
  "dev.autoqaPush.token": {
4949
4963
  type: "string",
4950
4964
  default: undefined,
4965
+ credential: true,
4951
4966
  },
4952
4967
 
4953
4968
  /**
@@ -5036,6 +5051,16 @@ export function getUi(path: SettingPath): AnyUiMetadata | undefined {
5036
5051
  return "ui" in def ? (def.ui as AnyUiMetadata) : undefined;
5037
5052
  }
5038
5053
 
5054
+ /**
5055
+ * True when a setting holds a credential and must never be printed in a bulk
5056
+ * dump. The single accessor every surface consults, so the CLI and the settings
5057
+ * panel cannot diverge on what counts as a secret.
5058
+ */
5059
+ export function isCredential(path: SettingPath): boolean {
5060
+ const def = SETTINGS_SCHEMA[path];
5061
+ return "credential" in def && def.credential === true;
5062
+ }
5063
+
5039
5064
  /** Get all paths for a specific tab */
5040
5065
  export function getPathsForTab(tab: SettingTab): SettingPath[] {
5041
5066
  return (Object.keys(SETTINGS_SCHEMA) as SettingPath[]).filter(path => {
@@ -701,6 +701,46 @@ export class Settings {
701
701
  }
702
702
  }
703
703
 
704
+ /**
705
+ * Read the config file for the read-modify-write save path.
706
+ *
707
+ * Unlike {@link Settings.#loadYaml}, a present-but-unusable file is NEVER
708
+ * reported as empty settings. `#saveNow` merges the modified keys into
709
+ * whatever this returns and writes the result back, so returning `{}` for a
710
+ * malformed or temporarily unreadable file would replace the user's entire
711
+ * configuration — broker tokens, model roles, provider entries — with the one
712
+ * key that happened to change. A missing file is the only safe empty base.
713
+ *
714
+ * An invalid file is moved aside first so the next save starts from a clean
715
+ * slate instead of failing forever, and the original bytes survive for
716
+ * recovery. Both failure modes throw; the caller re-queues the write.
717
+ */
718
+ async #loadYamlForWrite(filePath: string): Promise<RawSettings> {
719
+ let content: string;
720
+ try {
721
+ content = await Bun.file(filePath).text();
722
+ } catch (error) {
723
+ if (isEnoent(error)) return {};
724
+ throw new Error(`Failed to read settings config ${filePath}: ${String(error)}`);
725
+ }
726
+ try {
727
+ const parsed = YAML.parse(content);
728
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
729
+ return this.#migrateRawSettings(parsed as RawSettings);
730
+ } catch (error) {
731
+ const backupPath = `${filePath}.broken-${Date.now()}-${process.pid}`;
732
+ try {
733
+ await fs.promises.rename(filePath, backupPath);
734
+ } catch (renameError) {
735
+ throw new Error(
736
+ `Settings config is invalid and could not be moved aside: ${filePath}; refusing to overwrite it: ${String(renameError)}`,
737
+ );
738
+ }
739
+ logger.warn("Settings: moved invalid config aside", { path: filePath, backupPath, error: String(error) });
740
+ throw new Error(`Settings config is invalid: ${filePath} (moved to ${backupPath}): ${String(error)}`);
741
+ }
742
+ }
743
+
704
744
  async #loadProjectSettings(): Promise<RawSettings> {
705
745
  try {
706
746
  const result = await loadCapability(settingsCapability.id, { cwd: this.#cwd });
@@ -1282,8 +1322,9 @@ export class Settings {
1282
1322
 
1283
1323
  try {
1284
1324
  await withFileLock(configPath, async () => {
1285
- // Re-read to preserve external changes
1286
- const current = await this.#loadYaml(configPath);
1325
+ // Re-read to preserve external changes. Never treat an unreadable or
1326
+ // malformed file as empty here — that would overwrite it wholesale.
1327
+ const current = await this.#loadYamlForWrite(configPath);
1287
1328
 
1288
1329
  // Apply only our modified paths
1289
1330
  for (const modPath of modifiedPaths) {
@@ -111,6 +111,7 @@ export const BUNDLED_PI_REGISTRY_KEYS: ReadonlySet<string> = new Set([
111
111
  "@oh-my-pi/pi-ai/utils/deterministic-id",
112
112
  "@oh-my-pi/pi-ai/utils/empty-completion-retry",
113
113
  "@oh-my-pi/pi-ai/utils/event-stream",
114
+ "@oh-my-pi/pi-ai/utils/fences",
114
115
  "@oh-my-pi/pi-ai/utils/foundry",
115
116
  "@oh-my-pi/pi-ai/utils/google-validation",
116
117
  "@oh-my-pi/pi-ai/utils/http-inspector",
@@ -135,6 +135,7 @@ import * as bundledPiAiUtilsBlockSymbols from "jeopi-ai/utils/block-symbols";
135
135
  import * as bundledPiAiUtilsDeterministicId from "jeopi-ai/utils/deterministic-id";
136
136
  import * as bundledPiAiUtilsEmptyCompletionRetry from "jeopi-ai/utils/empty-completion-retry";
137
137
  import * as bundledPiAiUtilsEventStream from "jeopi-ai/utils/event-stream";
138
+ import * as bundledPiAiUtilsFences from "jeopi-ai/utils/fences";
138
139
  import * as bundledPiAiUtilsFoundry from "jeopi-ai/utils/foundry";
139
140
  import * as bundledPiAiUtilsGoogleValidation from "jeopi-ai/utils/google-validation";
140
141
  import * as bundledPiAiUtilsHarmonyLeak from "jeopi-ai/utils/harmony-leak";
@@ -1265,6 +1266,7 @@ export const BUNDLED_PI_REGISTRY: Readonly<Record<string, Readonly<Record<string
1265
1266
  Record<string, unknown>
1266
1267
  >,
1267
1268
  "@oh-my-pi/pi-ai/utils/event-stream": bundledPiAiUtilsEventStream as unknown as Readonly<Record<string, unknown>>,
1269
+ "@oh-my-pi/pi-ai/utils/fences": bundledPiAiUtilsFences as unknown as Readonly<Record<string, unknown>>,
1268
1270
  "@oh-my-pi/pi-ai/utils/foundry": bundledPiAiUtilsFoundry as unknown as Readonly<Record<string, unknown>>,
1269
1271
  "@oh-my-pi/pi-ai/utils/google-validation": bundledPiAiUtilsGoogleValidation as unknown as Readonly<
1270
1272
  Record<string, unknown>