jeopi-cli 16.2.13 → 16.2.15

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 (182) hide show
  1. package/CHANGELOG.md +21 -3
  2. package/README.md +1 -1
  3. package/dist/cli.js +3087 -3060
  4. package/dist/types/advisor/watchdog.d.ts +2 -2
  5. package/dist/types/autolearn/managed-skills.d.ts +2 -2
  6. package/dist/types/capability/types.d.ts +2 -2
  7. package/dist/types/cli/commands/migrate-legacy.d.ts +1 -0
  8. package/dist/types/cli/config-cli.d.ts +1 -1
  9. package/dist/types/cli/flag-tables.d.ts +1 -1
  10. package/dist/types/cli/grievances-cli.d.ts +1 -1
  11. package/dist/types/cli/models-cli.d.ts +1 -1
  12. package/dist/types/cli/update-cli.d.ts +2 -2
  13. package/dist/types/cli/worktree-cli.d.ts +1 -1
  14. package/dist/types/commit/index.d.ts +1 -1
  15. package/dist/types/commit/pipeline.d.ts +1 -1
  16. package/dist/types/commit/types.d.ts +1 -1
  17. package/dist/types/config/prompt-templates.d.ts +1 -1
  18. package/dist/types/config/settings-schema.d.ts +16 -6
  19. package/dist/types/config.d.ts +7 -7
  20. package/dist/types/discovery/helpers.d.ts +8 -8
  21. package/dist/types/discovery/omp-extension-roots.d.ts +2 -2
  22. package/dist/types/eval/py/runtime.d.ts +1 -1
  23. package/dist/types/extensibility/custom-tools/loader.d.ts +2 -2
  24. package/dist/types/extensibility/extensions/loader.d.ts +1 -1
  25. package/dist/types/extensibility/hooks/loader.d.ts +1 -1
  26. package/dist/types/extensibility/plugins/loader.d.ts +2 -2
  27. package/dist/types/extensibility/plugins/types.d.ts +13 -3
  28. package/dist/types/lsp/config.d.ts +2 -2
  29. package/dist/types/mcp/client.d.ts +1 -1
  30. package/dist/types/mcp/config-writer.d.ts +1 -1
  31. package/dist/types/mcp/oauth-flow.d.ts +1 -1
  32. package/dist/types/mcp/transports/http.d.ts +1 -1
  33. package/dist/types/memories/okf.d.ts +36 -0
  34. package/dist/types/modes/components/__tests__/assistant-agent-label.test.d.ts +1 -0
  35. package/dist/types/modes/components/assistant-message.d.ts +4 -0
  36. package/dist/types/modes/interactive-mode.d.ts +1 -1
  37. package/dist/types/sdk.d.ts +5 -5
  38. package/dist/types/session/agent-session.d.ts +3 -0
  39. package/dist/types/session/auth-broker-config.d.ts +4 -4
  40. package/dist/types/session/snapcompact-savings-journal.d.ts +1 -1
  41. package/dist/types/task/commands.d.ts +1 -1
  42. package/dist/types/task/critic-gate.d.ts +76 -0
  43. package/dist/types/task/critic-gate.test.d.ts +1 -0
  44. package/dist/types/task/discovery.d.ts +1 -1
  45. package/dist/types/task/executor.d.ts +1 -1
  46. package/dist/types/tools/acp-bridge.d.ts +2 -2
  47. package/dist/types/tools/browser/tab-protocol.d.ts +1 -1
  48. package/dist/types/tools/index.d.ts +6 -1
  49. package/dist/types/tools/plan-mode-guard.d.ts +8 -0
  50. package/dist/types/tools/report-tool-issue.d.ts +1 -1
  51. package/dist/types/utils/changelog.d.ts +2 -2
  52. package/dist/types/utils/image-loading.d.ts +4 -4
  53. package/dist/types/utils/image-resize.d.ts +1 -1
  54. package/dist/types/web/search/index.d.ts +1 -1
  55. package/examples/custom-tools/README.md +1 -1
  56. package/examples/extensions/README.md +1 -1
  57. package/examples/extensions/pirate.ts +1 -1
  58. package/examples/extensions/plan-mode.ts +1 -1
  59. package/examples/extensions/tools.ts +1 -1
  60. package/examples/hooks/README.md +1 -1
  61. package/examples/sdk/01-minimal.ts +1 -1
  62. package/examples/sdk/04-skills.ts +1 -1
  63. package/examples/sdk/06-extensions.ts +3 -3
  64. package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
  65. package/examples/sdk/12-redis-sessions.ts +1 -1
  66. package/examples/sdk/13-sql-sessions.ts +1 -1
  67. package/examples/sdk/README.md +1 -1
  68. package/package.json +12 -12
  69. package/scripts/jeopi +1 -1
  70. package/src/advisor/__tests__/config.test.ts +4 -4
  71. package/src/advisor/watchdog.ts +8 -8
  72. package/src/autolearn/managed-skills.ts +4 -4
  73. package/src/autoresearch/storage.ts +2 -2
  74. package/src/capability/types.ts +2 -2
  75. package/src/cli/agents-cli.ts +2 -2
  76. package/src/cli/args.ts +3 -3
  77. package/src/cli/auth-broker-cli.ts +5 -5
  78. package/src/cli/auth-gateway-cli.ts +4 -4
  79. package/src/cli/claude-trace-cli.ts +1 -1
  80. package/src/cli/commands/migrate-legacy.ts +24 -0
  81. package/src/cli/config-cli.ts +20 -5
  82. package/src/cli/flag-tables.ts +1 -1
  83. package/src/cli/gallery-fixtures/misc.ts +7 -7
  84. package/src/cli/gallery-screenshot.ts +2 -2
  85. package/src/cli/grievances-cli.ts +1 -1
  86. package/src/cli/models-cli.ts +1 -1
  87. package/src/cli/plugin-cli.ts +2 -2
  88. package/src/cli/profile-alias.ts +8 -8
  89. package/src/cli/read-cli.ts +1 -1
  90. package/src/cli/ssh-cli.ts +2 -2
  91. package/src/cli/update-cli.ts +27 -27
  92. package/src/cli/worktree-cli.ts +3 -3
  93. package/src/cli.ts +5 -5
  94. package/src/commands/agents.ts +2 -2
  95. package/src/commands/auth-broker.ts +1 -1
  96. package/src/commands/config.ts +1 -1
  97. package/src/commands/dry-balance.ts +6 -6
  98. package/src/commands/launch.ts +2 -2
  99. package/src/commands/models.ts +1 -1
  100. package/src/commands/say.ts +1 -1
  101. package/src/commands/token.ts +5 -5
  102. package/src/commands/ttsr.ts +2 -2
  103. package/src/commands/worktree.ts +3 -3
  104. package/src/commit/agentic/prompts/system.md +1 -1
  105. package/src/commit/cli.ts +1 -1
  106. package/src/commit/index.ts +1 -1
  107. package/src/commit/pipeline.ts +1 -1
  108. package/src/commit/types.ts +1 -1
  109. package/src/config/mcp-schema.json +5 -5
  110. package/src/config/models-config-schema.ts +1 -1
  111. package/src/config/prompt-templates.ts +2 -2
  112. package/src/config/settings-schema.ts +20 -8
  113. package/src/config/settings.ts +1 -1
  114. package/src/config.ts +10 -10
  115. package/src/dap/session.ts +2 -2
  116. package/src/debug/protocol-probe.ts +1 -1
  117. package/src/debug/raw-sse-buffer.ts +3 -3
  118. package/src/debug/raw-sse.ts +2 -2
  119. package/src/discovery/builtin.ts +9 -9
  120. package/src/discovery/helpers.ts +13 -13
  121. package/src/discovery/omp-extension-roots.ts +4 -4
  122. package/src/discovery/omp-plugins.ts +2 -2
  123. package/src/discovery/substitute-plugin-root.ts +3 -3
  124. package/src/eval/py/runtime.ts +1 -1
  125. package/src/eval/rb/runner.rb +6 -6
  126. package/src/export/custom-share.ts +1 -1
  127. package/src/extensibility/custom-tools/loader.ts +3 -3
  128. package/src/extensibility/extensions/loader.ts +4 -4
  129. package/src/extensibility/hooks/loader.ts +1 -1
  130. package/src/extensibility/plugins/loader.ts +16 -10
  131. package/src/extensibility/plugins/manager.ts +10 -9
  132. package/src/extensibility/plugins/types.ts +15 -4
  133. package/src/internal-urls/docs-index.generated.txt +1 -1
  134. package/src/lsp/config.ts +4 -4
  135. package/src/lsp/index.ts +1 -1
  136. package/src/main.ts +1 -1
  137. package/src/mcp/client.ts +1 -1
  138. package/src/mcp/config-writer.ts +2 -2
  139. package/src/mcp/manager.ts +1 -1
  140. package/src/mcp/oauth-flow.ts +1 -1
  141. package/src/mcp/timeout.ts +2 -2
  142. package/src/mcp/transports/http.ts +1 -1
  143. package/src/memories/index.ts +82 -11
  144. package/src/memories/okf.ts +129 -0
  145. package/src/modes/components/__tests__/assistant-agent-label.test.ts +87 -0
  146. package/src/modes/components/assistant-message.ts +27 -0
  147. package/src/modes/components/extensions/extension-dashboard.ts +1 -1
  148. package/src/modes/controllers/mcp-command-controller.ts +2 -2
  149. package/src/modes/controllers/omfg-controller.ts +2 -2
  150. package/src/modes/controllers/ssh-command-controller.ts +2 -2
  151. package/src/modes/interactive-mode.ts +3 -1
  152. package/src/modes/setup-wizard/index.ts +1 -1
  153. package/src/prompts/agents/jeo.md +20 -10
  154. package/src/prompts/system/autolearn-guidance.md +2 -2
  155. package/src/prompts/system/system-prompt.md +10 -1
  156. package/src/prompts/tools/learn.md +1 -1
  157. package/src/prompts/tools/manage-skill.md +1 -1
  158. package/src/sdk.ts +8 -6
  159. package/src/secrets/index.ts +2 -2
  160. package/src/session/agent-session.ts +86 -6
  161. package/src/session/auth-broker-config.ts +6 -6
  162. package/src/session/snapcompact-savings-journal.ts +1 -1
  163. package/src/slash-commands/helpers/mcp.ts +1 -1
  164. package/src/system-prompt.test.ts +14 -14
  165. package/src/task/commands.ts +1 -1
  166. package/src/task/critic-gate.test.ts +181 -0
  167. package/src/task/critic-gate.ts +148 -0
  168. package/src/task/discovery.ts +5 -5
  169. package/src/task/executor.ts +1 -1
  170. package/src/task/index.ts +35 -2
  171. package/src/tools/acp-bridge.ts +2 -2
  172. package/src/tools/browser/launch.ts +1 -1
  173. package/src/tools/browser/tab-protocol.ts +1 -1
  174. package/src/tools/grep.ts +2 -2
  175. package/src/tools/index.ts +6 -1
  176. package/src/tools/plan-mode-guard.ts +16 -0
  177. package/src/tools/read.ts +1 -1
  178. package/src/tools/report-tool-issue.ts +2 -2
  179. package/src/utils/changelog.ts +2 -2
  180. package/src/utils/image-loading.ts +4 -4
  181. package/src/utils/image-resize.ts +5 -5
  182. package/src/web/search/index.ts +1 -1
package/src/lsp/config.ts CHANGED
@@ -336,7 +336,7 @@ function getConfigSources(cwd: string): ConfigSource[] {
336
336
  sources.push(fileConfigSource(path.join(cwd, filename)));
337
337
  }
338
338
 
339
- // Project config directories (.omp/, .pi/, .claude/)
339
+ // Project config directories (.jeopi/, .pi/, .claude/)
340
340
  const projectDirs = getConfigDirPaths("", { user: false, project: true, cwd });
341
341
  for (const dir of projectDirs) {
342
342
  for (const filename of filenames) {
@@ -344,7 +344,7 @@ function getConfigSources(cwd: string): ConfigSource[] {
344
344
  }
345
345
  }
346
346
 
347
- // User config directories (~/.omp/agent/, ~/.pi/agent/, ~/.claude/)
347
+ // User config directories (~/.jeopi/agent/, ~/.pi/agent/, ~/.claude/)
348
348
  const userDirs = getConfigDirPaths("", { user: true, project: false });
349
349
  for (const dir of userDirs) {
350
350
  for (const filename of filenames) {
@@ -374,8 +374,8 @@ function getConfigSources(cwd: string): ConfigSource[] {
374
374
  *
375
375
  * Priority (highest to lowest):
376
376
  * 1. Project root: lsp.json/.lsp.json/lsp.yml/.lsp.yml/lsp.yaml/.lsp.yaml
377
- * 2. Project config dirs: .omp/lsp.*, .pi/lsp.*, .claude/lsp.* (+ hidden variants)
378
- * 3. User config dirs: ~/.omp/agent/lsp.*, ~/.pi/agent/lsp.*, ~/.claude/lsp.* (+ hidden variants)
377
+ * 2. Project config dirs: .jeopi/lsp.*, .pi/lsp.*, .claude/lsp.* (+ hidden variants)
378
+ * 3. User config dirs: ~/.jeopi/agent/lsp.*, ~/.pi/agent/lsp.*, ~/.claude/lsp.* (+ hidden variants)
379
379
  * 4. User home root: ~/lsp.*, ~/.lsp.*
380
380
  * 5. Auto-detect from project markers + available binaries
381
381
  *
package/src/lsp/index.ts CHANGED
@@ -2060,7 +2060,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
2060
2060
 
2061
2061
  if (action === "reload" && (isWorkspace || !resolvedFile)) {
2062
2062
  // `reload *` is the user's explicit request to re-read config from
2063
- // disk. Drop the per-cwd cache entry so `.omp/lsp.json`, root markers,
2063
+ // disk. Drop the per-cwd cache entry so `.jeopi/lsp.json`, root markers,
2064
2064
  // and plugin configs added after the first LSP call become visible —
2065
2065
  // otherwise `getConfig` returns the first observation for the rest of
2066
2066
  // the process lifetime (#3546).
package/src/main.ts CHANGED
@@ -747,7 +747,7 @@ export async function createSessionManager(
747
747
 
748
748
  /** Discover SYSTEM.md file if no CLI system prompt was provided */
749
749
  function discoverSystemPromptFile(): string | undefined {
750
- // Check project-local first (.omp/SYSTEM.md, .pi/SYSTEM.md legacy)
750
+ // Check project-local first (.jeopi/SYSTEM.md, .pi/SYSTEM.md legacy)
751
751
  const projectPath = findConfigFile("SYSTEM.md", { user: false });
752
752
  if (projectPath) {
753
753
  return projectPath;
package/src/mcp/client.ts CHANGED
@@ -129,7 +129,7 @@ async function initializeConnection(
129
129
  /**
130
130
  * Connect to an MCP server.
131
131
  * Has a 30 second timeout by default to prevent blocking startup.
132
- * Set OMP_MCP_TIMEOUT_MS=0 to disable MCP client-side timeouts.
132
+ * Set JEOPI_MCP_TIMEOUT_MS=0 to disable MCP client-side timeouts.
133
133
  */
134
134
  export async function connectToServer(
135
135
  name: string,
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * MCP Configuration File Writer
3
3
  *
4
- * Utilities for reading/writing .omp/mcp.json files at user or project level.
4
+ * Utilities for reading/writing .jeopi/mcp.json files at user or project level.
5
5
  */
6
6
  import * as fs from "node:fs";
7
7
  import * as path from "node:path";
@@ -270,7 +270,7 @@ export interface SetMcpServerEnabledOptions {
270
270
  projectPath: string;
271
271
  /**
272
272
  * Absolute path to the loaded row's source mcp.json. Provide ONLY for
273
- * formats this codebase owns (native `.omp/mcp.json` and `mcp-json`
273
+ * formats this codebase owns (native `.jeopi/mcp.json` and `mcp-json`
274
274
  * `mcp.json`/`.mcp.json`). Tool-owned configs (opencode.json, claude.json,
275
275
  * settings.json …) MUST be omitted; we never mutate another tool's file.
276
276
  */
@@ -524,7 +524,7 @@ export class MCPManager {
524
524
  // Pending tasks without cached tools used to be awaited synchronously here,
525
525
  // which gated the entire UI on the slowest server's per-request timeout
526
526
  // (issue #2100: a single unresponsive MCP server blocked startup for the
527
- // full 30 s `OMP_MCP_TIMEOUT_MS`). Leave them in flight — the background
527
+ // full 30 s `JEOPI_MCP_TIMEOUT_MS`). Leave them in flight — the background
528
528
  // `void toolsPromise.then(...)` chain above registers their tools and
529
529
  // fires `#onToolsChanged` once the connect finishes, or logs the failure
530
530
  // after `allowBackgroundLogging` flips below.
@@ -19,7 +19,7 @@ const MCP_OAUTH_URL_CREDENTIAL_PREFIX = "mcp_oauth:";
19
19
  const MCP_OAUTH_PROFILE_CREDENTIAL_PREFIX = `${MCP_OAUTH_URL_CREDENTIAL_PREFIX}profile:`;
20
20
 
21
21
  /**
22
- * Deterministic credential id for an MCP server URL scoped to an OMP profile.
22
+ * Deterministic credential id for an MCP server URL scoped to a jeopi profile.
23
23
  *
24
24
  * Local profile stores are already separate, but auth-broker storage shares one
25
25
  * provider namespace across profiles. Including the profile in the provider key
@@ -1,7 +1,7 @@
1
1
  import { logger } from "jeopi-utils";
2
2
 
3
3
  const DEFAULT_MCP_TIMEOUT_MS = 30_000;
4
- const MCP_TIMEOUT_ENV = "OMP_MCP_TIMEOUT_MS";
4
+ const MCP_TIMEOUT_ENV = "JEOPI_MCP_TIMEOUT_MS";
5
5
 
6
6
  let neverAbortController: AbortController | undefined;
7
7
 
@@ -10,7 +10,7 @@ export function resolveMCPTimeoutMs(configTimeout?: number): number {
10
10
  if (raw) {
11
11
  const value = Number(raw);
12
12
  if (Number.isFinite(value) && value >= 0) return value;
13
- logger.warn("Ignoring invalid OMP_MCP_TIMEOUT_MS env value; expected a non-negative number", {
13
+ logger.warn("Ignoring invalid JEOPI_MCP_TIMEOUT_MS env value; expected a non-negative number", {
14
14
  value: raw,
15
15
  });
16
16
  }
@@ -24,7 +24,7 @@ const HTTP_SSE_CONNECT_TIMEOUT_MS = 1_000;
24
24
  * Best-effort startup deadline for the optional Streamable HTTP GET SSE listener.
25
25
  *
26
26
  * Returns `0` (disabled) when the operator has explicitly disabled MCP client-side
27
- * timeouts via `timeout: 0` or `OMP_MCP_TIMEOUT_MS=0`, mirroring the rest of the
27
+ * timeouts via `timeout: 0` or `JEOPI_MCP_TIMEOUT_MS=0`, mirroring the rest of the
28
28
  * MCP timeout surface. Otherwise caps the wait at one second and scales below
29
29
  * short request timeouts so connect-time never exceeds the request budget.
30
30
  */
@@ -17,6 +17,7 @@ import readPathTemplate from "../prompts/memories/read-path.md" with { type: "te
17
17
  import stageOneInputTemplate from "../prompts/memories/stage_one_input.md" with { type: "text" };
18
18
  import stageOneSystemTemplate from "../prompts/memories/stage_one_system.md" with { type: "text" };
19
19
  import type { AgentSession } from "../session/agent-session";
20
+ import { okfTimestamp, parseOkfDocument, renderOkfDocument, stripOkfFrontmatter } from "./okf";
20
21
  import {
21
22
  claimStage1Jobs,
22
23
  clearMemoryData as clearMemoryDataInDb,
@@ -186,7 +187,7 @@ async function readMemoryToolDeveloperInstructionsSnapshot(
186
187
 
187
188
  let summary = "";
188
189
  try {
189
- summary = (await Bun.file(path.join(memoryRoot, "memory_summary.md")).text()).trim();
190
+ summary = stripOkfFrontmatter(await Bun.file(path.join(memoryRoot, "memory_summary.md")).text()).trim();
190
191
  } catch {
191
192
  // Missing or unreadable summary — injection is best-effort; fall through
192
193
  // so any captured lessons still surface on their own.
@@ -790,10 +791,18 @@ async function syncPhase2Artifacts(memoryRoot: string, outputs: Stage1OutputRow[
790
791
  const stem = formatRolloutFilename(row.threadId, row.rolloutSlug);
791
792
  const filename = `${stem}.md`;
792
793
  keepFiles.add(filename);
793
- const body = [`thread_id: ${row.threadId}`, `updated_at: ${row.sourceUpdatedAt}`, "", row.rolloutSummary].join(
794
- "\n",
794
+ const document = renderOkfDocument(
795
+ {
796
+ type: "Session Summary",
797
+ title: `Rollout ${stem}`,
798
+ description: "Summary of one jeopi session rollout, feeding memory consolidation.",
799
+ tags: ["jeopi", "memory", "rollout"],
800
+ timestamp: okfTimestamp(row.sourceUpdatedAt),
801
+ extra: { thread_id: row.threadId, updated_at: row.sourceUpdatedAt },
802
+ },
803
+ row.rolloutSummary,
795
804
  );
796
- await Bun.write(path.join(summariesDir, filename), `${body.trim()}\n`);
805
+ await Bun.write(path.join(summariesDir, filename), document);
797
806
  }
798
807
 
799
808
  const currentFiles = await fs.readdir(summariesDir).catch(() => [] as string[]);
@@ -804,7 +813,17 @@ async function syncPhase2Artifacts(memoryRoot: string, outputs: Stage1OutputRow[
804
813
  }
805
814
 
806
815
  const rawBody = buildRawMemoriesMarkdown(outputs);
807
- await Bun.write(path.join(memoryRoot, "raw_memories.md"), rawBody);
816
+ const rawDocument = renderOkfDocument(
817
+ {
818
+ type: "Raw Memories",
819
+ title: "jeopi Raw Memories",
820
+ description: "Per-thread raw memory extracts feeding the consolidation pass.",
821
+ tags: ["jeopi", "memory", "raw"],
822
+ timestamp: okfTimestamp(),
823
+ },
824
+ rawBody,
825
+ );
826
+ await Bun.write(path.join(memoryRoot, "raw_memories.md"), rawDocument);
808
827
  }
809
828
 
810
829
  async function cleanupConsolidatedArtifacts(memoryRoot: string): Promise<void> {
@@ -837,7 +856,18 @@ async function readRolloutSummaries(memoryRoot: string): Promise<string> {
837
856
  .text()
838
857
  .catch(() => "");
839
858
  if (!text.trim()) continue;
840
- blocks.push(`--- ${name} ---\n${text.trim()}`);
859
+ // OKF atoms carry thread identity in frontmatter; rebuild the plain
860
+ // header the consolidation prompt has always seen. Legacy files (no
861
+ // frontmatter) already carry that header inline and pass verbatim.
862
+ const { frontmatter, body } = parseOkfDocument(text);
863
+ const threadId = typeof frontmatter?.thread_id === "string" ? frontmatter.thread_id : undefined;
864
+ const updatedAt = typeof frontmatter?.updated_at === "number" ? frontmatter.updated_at : undefined;
865
+ const content =
866
+ threadId !== undefined
867
+ ? [`thread_id: ${threadId}`, `updated_at: ${updatedAt ?? ""}`, "", body.trim()].join("\n")
868
+ : body.trim();
869
+ if (!content.trim()) continue;
870
+ blocks.push(`--- ${name} ---\n${content.trim()}`);
841
871
  }
842
872
  if (blocks.length === 0) return "No rollout summaries yet.";
843
873
  return blocks.join("\n\n");
@@ -860,7 +890,7 @@ async function runConsolidationModel(options: {
860
890
  }>;
861
891
  }> {
862
892
  const { memoryRoot, model, apiKey } = options;
863
- const rawMemories = await Bun.file(path.join(memoryRoot, "raw_memories.md")).text();
893
+ const rawMemories = stripOkfFrontmatter(await Bun.file(path.join(memoryRoot, "raw_memories.md")).text());
864
894
  const rolloutSummaries = await readRolloutSummaries(memoryRoot);
865
895
  const input = prompt.render(consolidationTemplate, {
866
896
  raw_memories: truncateByApproxTokens(rawMemories, 20_000),
@@ -938,8 +968,35 @@ async function applyConsolidation(
938
968
  }>;
939
969
  },
940
970
  ): Promise<void> {
941
- await Bun.write(path.join(memoryRoot, "MEMORY.md"), `${consolidated.memoryMd.trim()}\n`);
942
- await Bun.write(path.join(memoryRoot, "memory_summary.md"), `${consolidated.memorySummary.trim()}\n`);
971
+ const timestamp = okfTimestamp();
972
+ await Bun.write(
973
+ path.join(memoryRoot, "MEMORY.md"),
974
+ renderOkfDocument(
975
+ {
976
+ type: "Memory",
977
+ title: "jeopi Consolidated Memory",
978
+ description: "Long-term consolidated memory jeopi maintains for this project.",
979
+ tags: ["jeopi", "memory", "consolidated"],
980
+ timestamp,
981
+ },
982
+ // The model occasionally emits its own frontmatter; strip it so the
983
+ // atom never carries a nested fence.
984
+ stripOkfFrontmatter(consolidated.memoryMd.trim()),
985
+ ),
986
+ );
987
+ await Bun.write(
988
+ path.join(memoryRoot, "memory_summary.md"),
989
+ renderOkfDocument(
990
+ {
991
+ type: "Memory Summary",
992
+ title: "jeopi Memory Summary",
993
+ description: "Prompt-time memory guidance injected at jeopi session start.",
994
+ tags: ["jeopi", "memory", "summary"],
995
+ timestamp,
996
+ },
997
+ stripOkfFrontmatter(consolidated.memorySummary.trim()),
998
+ ),
999
+ );
943
1000
  const skillsDir = path.join(memoryRoot, "skills");
944
1001
  await fs.mkdir(skillsDir, { recursive: true });
945
1002
  const keep = new Set<string>();
@@ -1341,23 +1398,37 @@ async function appendLearnedLine(filePath: string, line: string): Promise<void>
1341
1398
  } catch (err) {
1342
1399
  if (!isEnoent(err)) throw err;
1343
1400
  }
1401
+ // The `- ` filter keeps only lesson lines, so a legacy file (no
1402
+ // frontmatter) and an OKF atom (frontmatter re-rendered below) both fold
1403
+ // into the same newest-first list.
1344
1404
  const prior = existing
1345
1405
  .split("\n")
1346
1406
  .map(l => l.trim())
1347
1407
  .filter(l => l.startsWith("- ") && l !== line);
1348
1408
  const lessons = [line, ...prior].slice(0, MAX_LEARNED_LESSONS);
1349
- await Bun.write(filePath, `${lessons.join("\n")}\n`);
1409
+ const document = renderOkfDocument(
1410
+ {
1411
+ type: "Lessons",
1412
+ title: "jeopi Learned Lessons",
1413
+ description: "Lessons captured via the learn tool, newest first.",
1414
+ tags: ["jeopi", "memory", "lessons"],
1415
+ timestamp: okfTimestamp(),
1416
+ },
1417
+ lessons.join("\n"),
1418
+ );
1419
+ await Bun.write(filePath, document);
1350
1420
  }
1351
1421
 
1352
1422
  /**
1353
1423
  * Read `learned.md`, neutralizing each line on read too — a hand-edited or
1354
1424
  * pre-existing file bypasses write-time normalization and the block renders
1355
1425
  * unescaped into the system prompt. Returns "" when absent/unreadable.
1426
+ * OKF frontmatter is stripped so only lesson lines reach the prompt.
1356
1427
  */
1357
1428
  async function readLearnedLessons(memoryRoot: string): Promise<string> {
1358
1429
  let raw = "";
1359
1430
  try {
1360
- raw = (await Bun.file(path.join(memoryRoot, LEARNED_LESSONS_FILE)).text()).trim();
1431
+ raw = stripOkfFrontmatter(await Bun.file(path.join(memoryRoot, LEARNED_LESSONS_FILE)).text()).trim();
1361
1432
  } catch {
1362
1433
  return "";
1363
1434
  }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Open Knowledge Format (OKF v0.1) support for jeopi memory documents.
3
+ *
4
+ * OKF (https://github.com/google/open-knowledge-format — the format jeo-skills'
5
+ * `okf` skill validates and consumes) stores knowledge atoms as
6
+ * YAML-frontmatter Markdown: required `type` / `title` / `description`,
7
+ * optional `resource` / `tags` / `timestamp`. jeopi's local memory backend
8
+ * writes its artifacts (`MEMORY.md`, `memory_summary.md`, `raw_memories.md`,
9
+ * `rollout_summaries/*.md`, `learned.md`) as OKF atoms so any OKF consumer can
10
+ * read a jeopi memory root as a knowledge bundle.
11
+ *
12
+ * Prompt-injection read paths strip the frontmatter via
13
+ * {@link stripOkfFrontmatter} so the model-facing text stays unchanged;
14
+ * legacy files without frontmatter pass through verbatim.
15
+ */
16
+ import { YAML } from "bun";
17
+
18
+ /** OKF v0.1 frontmatter fields. Only `type`/`title`/`description` are required. */
19
+ export interface OkfFields {
20
+ type: string;
21
+ title: string;
22
+ description: string;
23
+ resource?: string;
24
+ tags?: readonly string[];
25
+ /** ISO 8601 datetime, e.g. `2026-07-02T10:00:00Z`. */
26
+ timestamp?: string;
27
+ /** Producer-specific extension fields (e.g. `thread_id`). */
28
+ extra?: Record<string, string | number>;
29
+ }
30
+
31
+ const FENCE = "---";
32
+
33
+ function yamlScalar(value: string | number): string {
34
+ if (typeof value === "number") return String(value);
35
+ return JSON.stringify(value);
36
+ }
37
+
38
+ /** Render an OKF atom: YAML frontmatter followed by the markdown body. */
39
+ export function renderOkfDocument(fields: OkfFields, body: string): string {
40
+ const lines: string[] = [FENCE];
41
+ lines.push(`type: ${yamlScalar(fields.type)}`);
42
+ lines.push(`title: ${yamlScalar(fields.title)}`);
43
+ lines.push(`description: ${yamlScalar(fields.description)}`);
44
+ if (fields.resource) lines.push(`resource: ${yamlScalar(fields.resource)}`);
45
+ if (fields.tags?.length) lines.push(`tags: [${fields.tags.join(", ")}]`);
46
+ if (fields.timestamp) lines.push(`timestamp: ${fields.timestamp}`);
47
+ for (const [key, value] of Object.entries(fields.extra ?? {})) {
48
+ lines.push(`${key}: ${yamlScalar(value)}`);
49
+ }
50
+ lines.push(FENCE, "");
51
+ const trimmedBody = body.trim();
52
+ return trimmedBody ? `${lines.join("\n")}\n${trimmedBody}\n` : `${lines.join("\n")}`;
53
+ }
54
+
55
+ /** Locate the closing fence line of a leading frontmatter block, or -1. */
56
+ function findFrontmatterEnd(lines: string[]): number {
57
+ if (lines[0]?.trim() !== FENCE) return -1;
58
+ for (let index = 1; index < lines.length; index++) {
59
+ if (lines[index].trim() === FENCE) return index;
60
+ }
61
+ return -1;
62
+ }
63
+
64
+ /** Parsed frontmatter (undefined when absent/unparseable) plus the body. */
65
+ export interface OkfDocumentParts {
66
+ frontmatter: Record<string, unknown> | undefined;
67
+ body: string;
68
+ }
69
+
70
+ /**
71
+ * Split an OKF document into frontmatter and body. Tolerant by design: text
72
+ * without a leading fence (legacy jeopi memory files, hand-edited files)
73
+ * returns the whole text as body with `frontmatter: undefined`, and a
74
+ * malformed YAML block still strips so prompt paths never leak fences.
75
+ */
76
+ export function parseOkfDocument(text: string): OkfDocumentParts {
77
+ const lines = text.split("\n");
78
+ const end = findFrontmatterEnd(lines);
79
+ if (end === -1) return { frontmatter: undefined, body: text };
80
+ const body = lines.slice(end + 1).join("\n");
81
+ let frontmatter: Record<string, unknown> | undefined;
82
+ try {
83
+ const parsed = YAML.parse(lines.slice(1, end).join("\n"));
84
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
85
+ frontmatter = parsed as Record<string, unknown>;
86
+ }
87
+ } catch {
88
+ // Malformed YAML — treat as fenceless body strip.
89
+ }
90
+ return { frontmatter, body };
91
+ }
92
+
93
+ /** Body of an OKF document with any leading frontmatter removed. */
94
+ export function stripOkfFrontmatter(text: string): string {
95
+ return parseOkfDocument(text).body;
96
+ }
97
+
98
+ /**
99
+ * Validate a document against the OKF v0.1 rules the jeo-skills `okf` linter
100
+ * enforces: a leading `---` fence, a closing fence, and non-empty
101
+ * `type` / `title` / `description` fields. Returns an empty array when valid.
102
+ */
103
+ export function validateOkfDocument(text: string): string[] {
104
+ const errors: string[] = [];
105
+ const lines = text.split("\n");
106
+ if (lines[0]?.trim() !== FENCE) {
107
+ return ["missing leading frontmatter fence (---)"];
108
+ }
109
+ const end = findFrontmatterEnd(lines);
110
+ if (end === -1) {
111
+ return ["unclosed frontmatter"];
112
+ }
113
+ const { frontmatter } = parseOkfDocument(text);
114
+ if (!frontmatter) {
115
+ return ["frontmatter is not a YAML mapping"];
116
+ }
117
+ for (const field of ["type", "title", "description"] as const) {
118
+ const value = frontmatter[field];
119
+ if (typeof value !== "string" || value.trim() === "") {
120
+ errors.push(`missing required field '${field}'`);
121
+ }
122
+ }
123
+ return errors;
124
+ }
125
+
126
+ /** Format a unix-seconds timestamp as the ISO 8601 string OKF expects. */
127
+ export function okfTimestamp(unixSeconds: number = Math.floor(Date.now() / 1000)): string {
128
+ return new Date(unixSeconds * 1000).toISOString().replace(/\.\d{3}Z$/, "Z");
129
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * jeo TUI identity contract: a name label on its own line leads assistant
3
+ * segments with visible prose (jeo-code's `agentLabel()` ported to jeopi).
4
+ * Tool-call-only blocks stay unlabeled, and an empty `ui.agentLabel` disables
5
+ * the label entirely.
6
+ */
7
+ import { afterEach, beforeAll, describe, expect, it } from "bun:test";
8
+ import type { AssistantMessage } from "jeopi-ai";
9
+ import { Settings } from "../../../config/settings";
10
+ import { getThemeByName, setThemeInstance, type Theme } from "../../theme/theme";
11
+ import { AssistantMessageComponent, setAssistantAgentLabel } from "../assistant-message";
12
+
13
+ const strip = (lines: readonly string[]): string =>
14
+ lines
15
+ .join("\n")
16
+ .replace(/\x1b\]8;[^\x1b\x07]*(?:\x07|\x1b\\)/g, "")
17
+ .replace(/\x1b\[[0-9;]*m/g, "");
18
+
19
+ function assistantMessage(content: AssistantMessage["content"]): AssistantMessage {
20
+ return {
21
+ role: "assistant",
22
+ content,
23
+ stopReason: "stop",
24
+ usage: {
25
+ input: 0,
26
+ output: 0,
27
+ cacheRead: 0,
28
+ cacheWrite: 0,
29
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
30
+ },
31
+ timestamp: Date.now(),
32
+ } as AssistantMessage;
33
+ }
34
+
35
+ describe("assistant agent label (jeo identity)", () => {
36
+ let uiTheme: Theme;
37
+
38
+ beforeAll(async () => {
39
+ await Settings.init({ inMemory: true });
40
+ const loaded = await getThemeByName("dark");
41
+ if (!loaded) throw new Error("theme unavailable");
42
+ uiTheme = loaded;
43
+ setThemeInstance(uiTheme);
44
+ });
45
+
46
+ afterEach(() => {
47
+ setAssistantAgentLabel("jeo");
48
+ });
49
+
50
+ it("leads a reply with the label on its own line", () => {
51
+ const component = new AssistantMessageComponent(
52
+ assistantMessage([{ type: "text", text: "The fix is in place." }]),
53
+ );
54
+ const lines = strip(component.render(80)).split("\n");
55
+ const labelLine = lines.findIndex(line => line.trim() === "jeo");
56
+ const proseLine = lines.findIndex(line => line.includes("The fix is in place."));
57
+ expect(labelLine).toBeGreaterThanOrEqual(0);
58
+ expect(proseLine).toBeGreaterThan(labelLine);
59
+ });
60
+
61
+ it("leads a visible thinking block with the label too", () => {
62
+ const component = new AssistantMessageComponent(
63
+ assistantMessage([{ type: "thinking", thinking: "Considering the edge cases first." }]),
64
+ false,
65
+ );
66
+ const text = strip(component.render(80));
67
+ expect(text).toContain("jeo");
68
+ expect(text).toContain("Considering the edge cases first.");
69
+ });
70
+
71
+ it("leaves tool-call-only blocks unlabeled", () => {
72
+ const component = new AssistantMessageComponent(
73
+ assistantMessage([
74
+ { type: "toolCall", id: "call-1", name: "read", arguments: { path: "a.ts" } },
75
+ ] as AssistantMessage["content"]),
76
+ );
77
+ expect(strip(component.render(80))).not.toContain("jeo");
78
+ });
79
+
80
+ it("an empty label disables the identity line", () => {
81
+ setAssistantAgentLabel("");
82
+ const component = new AssistantMessageComponent(assistantMessage([{ type: "text", text: "Done." }]));
83
+ const lines = strip(component.render(80)).split("\n");
84
+ expect(lines.some(line => line.trim() === "jeo")).toBe(false);
85
+ expect(lines.some(line => line.includes("Done."))).toBe(true);
86
+ });
87
+ });
@@ -163,6 +163,24 @@ export function resetThinkingSpeedTracker(): void {
163
163
  sharedSpeedTracker.reset();
164
164
  }
165
165
 
166
+ /**
167
+ * Agent identity label led on its own line before each assistant segment —
168
+ * jeo-code's TUI identity ported to jeopi: a bold accent `jeo` name leads
169
+ * thought blocks and replies so the transcript reads as jeo speaking.
170
+ * Configured from `ui.agentLabel` at session start; "" disables the label.
171
+ */
172
+ let assistantAgentLabel = "jeo";
173
+
174
+ /** Set the assistant identity label ("" disables). Called from interactive-mode startup. */
175
+ export function setAssistantAgentLabel(label: string): void {
176
+ assistantAgentLabel = label.trim();
177
+ }
178
+
179
+ /** Current assistant identity label ("" when disabled). */
180
+ export function getAssistantAgentLabel(): string {
181
+ return assistantAgentLabel;
182
+ }
183
+
166
184
  /**
167
185
  * Linear-interpolate two `#rrggbb` colors in sRGB space. `t` clamps to [0,1]:
168
186
  * `t = 0` → `from`, `t = 1` → `to`. Drives the streaming speed badge, fading
@@ -728,6 +746,15 @@ export class AssistantMessageComponent extends Container {
728
746
  resolveThinkingDisplay(c, this.proseOnlyThinking).visible),
729
747
  );
730
748
 
749
+ // jeo identity: a bold accent name label on its own line leads every
750
+ // assistant segment with visible prose or a live thinking pulse —
751
+ // mirrors jeo-code's TUI (`agentLabel()` leading thought blocks and the
752
+ // final reply). Tool-call-only blocks stay unlabeled; their cards carry
753
+ // their own identity.
754
+ if (assistantAgentLabel && (hasVisibleContent || this.#shouldAnimateThinking(message))) {
755
+ this.#contentContainer.addChild(new Text(theme.fg("accent", theme.bold(assistantAgentLabel)), 1, 0));
756
+ }
757
+
731
758
  // Render content in order
732
759
  let thinkingIndex = 0;
733
760
  for (let i = 0; i < message.content.length; i++) {
@@ -266,7 +266,7 @@ export class ExtensionDashboard implements Component {
266
266
  if (!sm) return;
267
267
 
268
268
  // MCP toggles route through the canonical denylist in
269
- // `~/.omp/agent/mcp.json` so `/mcp list`, the MCP runtime, and this
269
+ // `~/.jeopi/agent/mcp.json` so `/mcp list`, the MCP runtime, and this
270
270
  // dashboard agree on every server's enabled state (issue #3827).
271
271
  if (extensionId.startsWith("mcp:")) {
272
272
  void this.#toggleMcpExtension(extensionId, enabled, sm);
@@ -986,8 +986,8 @@ export class MCPCommandController {
986
986
  const usesMcpRemote = [config.command, ...(config.args ?? [])].some(part => part?.includes("mcp-remote"));
987
987
  throw new Error(
988
988
  usesMcpRemote
989
- ? `this server proxies OAuth through mcp-remote, which caches tokens machine-wide in ~/.mcp-auth (shared across every OMP profile). Clear ~/.mcp-auth to force a fresh login, or replace the proxy with ${httpHint} so OMP manages OAuth per profile.`
990
- : `stdio servers manage their own credentials, so OMP has no OAuth to reauthorize. If the service supports OAuth over HTTP, configure it as ${httpHint} instead.`,
989
+ ? `this server proxies OAuth through mcp-remote, which caches tokens machine-wide in ~/.mcp-auth (shared across every jeopi profile). Clear ~/.mcp-auth to force a fresh login, or replace the proxy with ${httpHint} so jeopi manages OAuth per profile.`
990
+ : `stdio servers manage their own credentials, so jeopi has no OAuth to reauthorize. If the service supports OAuth over HTTP, configure it as ${httpHint} instead.`,
991
991
  );
992
992
  }
993
993
  // First test if server actually needs auth by connecting without OAuth
@@ -32,8 +32,8 @@ interface GenerateCandidateOptions {
32
32
  type SaveCandidateResult = { kind: "saved" | "aborted" | "rejected" } | { kind: "amend"; feedback: string };
33
33
 
34
34
  const MAX_ATTEMPTS = 3;
35
- const PROJECT_OPTION = "This project (.omp/rules)";
36
- const GLOBAL_OPTION = "Global — all projects (~/.omp/agent/rules)";
35
+ const PROJECT_OPTION = "This project (.jeopi/rules)";
36
+ const GLOBAL_OPTION = "Global — all projects (~/.jeopi/agent/rules)";
37
37
  const AMEND_OPTION = "Amend with feedback…";
38
38
 
39
39
  export class OmfgController {
@@ -281,7 +281,7 @@ export class SSHCommandController {
281
281
 
282
282
  // Show user-level hosts
283
283
  if (userHosts.length > 0) {
284
- lines.push(theme.fg("accent", "User level") + theme.fg("muted", ` (~/.omp/agent/ssh.json):`));
284
+ lines.push(theme.fg("accent", "User level") + theme.fg("muted", ` (~/.jeopi/agent/ssh.json):`));
285
285
  for (const name of userHosts) {
286
286
  const config = userConfig.hosts![name];
287
287
  const details = this.#formatHostDetails(config);
@@ -292,7 +292,7 @@ export class SSHCommandController {
292
292
 
293
293
  // Show project-level hosts
294
294
  if (projectHosts.length > 0) {
295
- lines.push(theme.fg("accent", "Project level") + theme.fg("muted", ` (.omp/ssh.json):`));
295
+ lines.push(theme.fg("accent", "Project level") + theme.fg("muted", ` (.jeopi/ssh.json):`));
296
296
  for (const name of projectHosts) {
297
297
  const config = projectConfig.hosts![name];
298
298
  const details = this.#formatHostDetails(config);
@@ -122,7 +122,7 @@ import { getEditorCommand, openInEditor } from "../utils/external-editor";
122
122
  import { getSessionAccentAnsi, getSessionAccentHex } from "../utils/session-color";
123
123
  import { messageHasDisplayableThinking } from "../utils/thinking-display";
124
124
  import { popTerminalTitle, pushTerminalTitle, setSessionTerminalTitle } from "../utils/title-generator";
125
- import type { AssistantMessageComponent } from "./components/assistant-message";
125
+ import { type AssistantMessageComponent, setAssistantAgentLabel } from "./components/assistant-message";
126
126
  import type { BashExecutionComponent } from "./components/bash-execution";
127
127
  import { ChatBlock, type ChatBlockHost } from "./components/chat-block";
128
128
  import { CustomEditor } from "./components/custom-editor";
@@ -677,6 +677,8 @@ export class InteractiveMode implements InteractiveModeContext {
677
677
 
678
678
  this.hideThinkingBlock = settings.get("hideThinkingBlock");
679
679
  this.proseOnlyThinking = settings.get("proseOnlyThinking");
680
+ // jeo TUI identity: the accent name label leading assistant segments.
681
+ setAssistantAgentLabel(settings.get("ui.agentLabel") ?? "jeo");
680
682
 
681
683
  const hookCommands: SlashCommand[] = (
682
684
  this.session.extensionRunner?.getRegisteredCommands(BUILTIN_SLASH_COMMAND_RESERVED_NAMES) ?? []
@@ -42,7 +42,7 @@ export async function selectSetupScenes(
42
42
  if (!isTTY) return [];
43
43
  if (!options.force) {
44
44
  if (options.resuming) return [];
45
- if (setupSkipEnvEnabled(options.skipEnv ?? Bun.env.OMP_SKIP_SETUP)) return [];
45
+ if (setupSkipEnvEnabled(options.skipEnv ?? Bun.env.JEOPI_SKIP_SETUP)) return [];
46
46
  if (options.setupWizardEnabled === false) return [];
47
47
  }
48
48