jorgex-stack 1.0.16 → 1.0.18

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/dist/cli.js CHANGED
@@ -3003,7 +3003,8 @@ function parseFlags(args) {
3003
3003
  list: false,
3004
3004
  check: false,
3005
3005
  removeEngram: false,
3006
- positional: []
3006
+ positional: [],
3007
+ unknownFlags: []
3007
3008
  };
3008
3009
  const readValue = (index) => {
3009
3010
  const value = args[index + 1];
@@ -3039,6 +3040,7 @@ function parseFlags(args) {
3039
3040
  else if (arg === "--list") flags.list = true;
3040
3041
  else if (arg === "--check") flags.check = true;
3041
3042
  else if (arg === "--remove-engram") flags.removeEngram = true;
3043
+ else if (arg.startsWith("-")) flags.unknownFlags.push(arg);
3042
3044
  else flags.positional.push(arg);
3043
3045
  }
3044
3046
  return flags;
@@ -3119,6 +3121,7 @@ function parseCliArgs(argv) {
3119
3121
  const flags = parseFlags(isCommand ? rest : argv);
3120
3122
  if (first === "--help" || first === "-h" || flags.help) return { action: "help", command, flags };
3121
3123
  if (first === "--version" || first === "-v" || flags.version) return { action: "version", command, flags };
3124
+ if (flags.unknownFlags.length > 0) return { action: "unknown-flags", command, flags };
3122
3125
  return { action: "run", command, flags };
3123
3126
  }
3124
3127
  async function resolveRuntimes(flags) {
@@ -3172,6 +3175,18 @@ async function main() {
3172
3175
  process.exitCode = 1;
3173
3176
  return;
3174
3177
  }
3178
+ if (parsed.action === "unknown-flags") {
3179
+ const { unknownFlags } = parsed.flags;
3180
+ const plural = unknownFlags.length > 1;
3181
+ console.error(`Flag${plural ? "s" : ""} no reconocido${plural ? "s" : ""}: ${unknownFlags.join(", ")}`);
3182
+ console.error(
3183
+ `jorgex-stack v${VERSION} no reconoce ${plural ? "esos flags" : "ese flag"}. Si esperabas que existiera, puede que est\xE9s ejecutando un binario cacheado antiguo:
3184
+ pnpm dlx jorgex-stack@latest ...
3185
+ Flags disponibles: jorgex-stack --help`
3186
+ );
3187
+ process.exitCode = 1;
3188
+ return;
3189
+ }
3175
3190
  const { command, flags } = parsed;
3176
3191
  if (flags.targetDir !== void 0 && flags.agents.length !== 1) {
3177
3192
  console.error("--target-dir requiere exactamente un runtime en --agents.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jorgex-stack",
3
- "version": "1.0.16",
3
+ "version": "1.0.18",
4
4
  "description": "Harness multi-agente portable: instala la config JorgeX (agentes, skills, hooks, Engram, MCPs) en Claude Code, Codex CLI y OpenCode",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -18,7 +18,7 @@ import type { Plugin } from "@opencode-ai/plugin"
18
18
 
19
19
  declare const Bun: {
20
20
  which?: (bin: string) => string | null
21
- spawnSync: (args: string[]) => { exitCode: number; stdout?: { toString(): string } | string }
21
+ spawnSync: (args: string[], options?: Record<string, unknown>) => { exitCode: number; stdout?: { toString(): string } | string }
22
22
  spawn: (args: string[], options?: Record<string, unknown>) => unknown
23
23
  file: (path: string) => { exists: () => Promise<boolean> }
24
24
  }
@@ -102,7 +102,7 @@ async function isEngramRunning(): Promise<boolean> {
102
102
  function extractProjectName(directory: string): string {
103
103
  // Try git remote origin URL
104
104
  try {
105
- const result = Bun.spawnSync(["git", "-C", directory, "remote", "get-url", "origin"])
105
+ const result = Bun.spawnSync(["git", "-C", directory, "remote", "get-url", "origin"], { stdout: "pipe", stderr: "ignore", stdin: "ignore" })
106
106
  if (result.exitCode === 0) {
107
107
  const url = result.stdout?.toString().trim()
108
108
  if (url) {
@@ -114,7 +114,7 @@ function extractProjectName(directory: string): string {
114
114
 
115
115
  // Fallback: git root directory name (works in worktrees)
116
116
  try {
117
- const result = Bun.spawnSync(["git", "-C", directory, "rev-parse", "--show-toplevel"])
117
+ const result = Bun.spawnSync(["git", "-C", directory, "rev-parse", "--show-toplevel"], { stdout: "pipe", stderr: "ignore", stdin: "ignore" })
118
118
  if (result.exitCode === 0) {
119
119
  const root = result.stdout?.toString().trim()
120
120
  if (root) return root.split(/[\\/]/).pop() ?? "unknown"
@@ -140,11 +140,39 @@ function stripPrivateTags(str: string): string {
140
140
  return str.replace(/<private>[\s\S]*?<\/private>/gi, "[REDACTED]").trim()
141
141
  }
142
142
 
143
+ /**
144
+ * Log errors and warnings to OpenCode's TUI log.
145
+ * Errors in hooks must not propagate to the plugin surface — they are silently
146
+ * swallowed here so the TUI does not crash. The error is visible only in OpenCode's
147
+ * log stream (visible to the user via `opencode logs`).
148
+ */
149
+ async function logToOpenCode(client: unknown, level: string, message: string, extra?: unknown): Promise<void> {
150
+ if (typeof client !== "object" || client === null) return;
151
+ const app = (client as { app?: unknown }).app;
152
+ if (typeof app !== "object" || app === null) return;
153
+ const log = (app as { log?: unknown }).log;
154
+ if (typeof log !== "function") return;
155
+ const safeExtra = extra instanceof Error ? { message: extra.message, stack: extra.stack } : extra;
156
+ try {
157
+ await log.call(app, {
158
+ body: {
159
+ service: "engram",
160
+ level,
161
+ message,
162
+ extra: safeExtra,
163
+ },
164
+ });
165
+ } catch {
166
+ // Logging must never break the plugin path.
167
+ }
168
+ }
169
+
143
170
  // ─── Plugin Export ───────────────────────────────────────────────────────────
144
171
 
145
172
  export const Engram: Plugin = async (ctx) => {
146
173
  const oldProject = ctx.directory.split(/[\\/]/).pop() ?? "unknown"
147
174
  const project = extractProjectName(ctx.directory)
175
+ const client = (ctx as { client?: unknown }).client
148
176
  const engramBin = resolveEngramBin()
149
177
 
150
178
  // Track tool counts per session (in-memory only, not critical)
@@ -228,10 +256,21 @@ export const Engram: Plugin = async (ctx) => {
228
256
  // Manifest doesn't exist or binary not found — silently skip
229
257
  }
230
258
 
259
+ // ─── Safe Hook Wrapper ─────────────────────────────────────────
260
+ // Wraps every hook to catch errors and log them to OpenCode instead
261
+ // of letting them propagate. This prevents malformed payloads or
262
+ // missing fields (output.context, output.system, etc.) from crashing
263
+ // the TUI. Errors are logged and swallowed (promise resolves cleanly).
264
+ const safe = <A extends unknown[]>(name: string, fn: (...args: A) => Promise<void>) =>
265
+ async (...args: A): Promise<void> => {
266
+ try { await fn(...args) }
267
+ catch (error) { void logToOpenCode(client, "error", `Engram hook ${name} failed`, error) }
268
+ }
269
+
231
270
  return {
232
271
  // ─── Event Listeners ───────────────────────────────────────────
233
272
 
234
- event: async ({ event }: { event: { type: string; properties?: unknown } }) => {
273
+ event: safe("event", async ({ event }: { event: { type: string; properties?: unknown } }) => {
235
274
  // --- Session Created ---
236
275
  if (event.type === "session.created") {
237
276
  // Bug fix (#116): session data is nested under event.properties.info,
@@ -271,7 +310,7 @@ export const Engram: Plugin = async (ctx) => {
271
310
  }
272
311
  }
273
312
 
274
- },
313
+ }),
275
314
 
276
315
  // ─── User Prompt Capture ──────────────────────────────────────
277
316
  // chat.message is called once per user message, before the LLM sees it.
@@ -279,7 +318,7 @@ export const Engram: Plugin = async (ctx) => {
279
318
  // output.message is typed as UserMessage (role:"user" already guaranteed).
280
319
  // output.parts contains TextPart[] with the actual message text.
281
320
 
282
- "chat.message": async (input: any, output: any) => {
321
+ "chat.message": safe("chat.message", async (input: any, output: any) => {
283
322
  // Skip sub-agent sessions — they inflate session counts (issue #116)
284
323
  if (subAgentSessions.has(input.sessionID)) return
285
324
 
@@ -313,7 +352,7 @@ export const Engram: Plugin = async (ctx) => {
313
352
  })
314
353
  }
315
354
  }
316
- },
355
+ }),
317
356
 
318
357
  // ─── Tool Execution Hook ─────────────────────────────────────
319
358
  // Count tool calls per session (for session end stats).
@@ -321,7 +360,7 @@ export const Engram: Plugin = async (ctx) => {
321
360
  // Passive capture: when a Task tool completes, POST its output to
322
361
  // the passive capture endpoint so the server extracts learnings.
323
362
 
324
- "tool.execute.after": async (input: any, output: any) => {
363
+ "tool.execute.after": safe("tool.execute.after", async (input: any, output: any) => {
325
364
  if (ENGRAM_TOOLS.has(input.tool.toLowerCase())) return
326
365
 
327
366
  // input.sessionID comes from OpenCode — always available
@@ -347,7 +386,7 @@ export const Engram: Plugin = async (ctx) => {
347
386
  })
348
387
  }
349
388
  }
350
- },
389
+ }),
351
390
 
352
391
  // ─── System Prompt: Always-on memory instructions ──────────
353
392
  // Injects MEMORY_INSTRUCTIONS into the system prompt of every message.
@@ -359,13 +398,13 @@ export const Engram: Plugin = async (ctx) => {
359
398
  // block at the beginning. By concatenating, we avoid adding extra system
360
399
  // messages that would break these models. See: GitHub issue #23.
361
400
 
362
- "experimental.chat.system.transform": async (_input: any, output: any) => {
401
+ "experimental.chat.system.transform": safe("experimental.chat.system.transform", async (_input: any, output: any) => {
363
402
  if (output.system.length > 0) {
364
403
  output.system[output.system.length - 1] += "\n\n" + MEMORY_INSTRUCTIONS
365
404
  } else {
366
405
  output.system.push(MEMORY_INSTRUCTIONS)
367
406
  }
368
- },
407
+ }),
369
408
 
370
409
  // ─── Compaction Hook: Persist memory + inject context ──────────
371
410
  // Compaction is triggered by the system (not the agent) when context
@@ -375,7 +414,7 @@ export const Engram: Plugin = async (ctx) => {
375
414
  // 2. Inject context from previous sessions into the compaction prompt
376
415
  // 3. Tell the compressor to remind the new agent to save memories
377
416
 
378
- "experimental.session.compacting": async (input: any, output: any) => {
417
+ "experimental.session.compacting": safe("experimental.session.compacting", async (input: any, output: any) => {
379
418
  if (input.sessionID) {
380
419
  await ensureSession(input.sessionID)
381
420
  }
@@ -400,6 +439,6 @@ export const Engram: Plugin = async (ctx) => {
400
439
  `Do this BEFORE any other work. After that, delegate to the global engram subagent to recover only the relevant previous context."\n\n` +
401
440
  `This is NOT optional. Without this, everything done before compaction is lost from memory.`
402
441
  )
403
- },
442
+ }),
404
443
  }
405
444
  }
@@ -44,16 +44,31 @@ export function createOpenCodeGoalHooks(deps: OpenCodeGoalHooksDeps): OpenCodeGo
44
44
  project: deps.project,
45
45
  });
46
46
 
47
+ // ─── Safe Hook Wrapper ─────────────────────────────────────────
48
+ // Wraps every hook to catch errors and log them via the logger instead
49
+ // of letting them propagate. This prevents OpenCode from crashing when
50
+ // Goal Mode hooks encounter malformed payloads or internal errors.
51
+ // Errors are logged (if a logger is provided) and swallowed (promise
52
+ // resolves cleanly even on failure).
53
+ const safe = <A extends unknown[]>(name: string, fn: (...args: A) => Promise<void>) =>
54
+ async (...args: A): Promise<void> => {
55
+ try {
56
+ await fn(...args);
57
+ } catch (error) {
58
+ try { deps.logger?.error?.(`Goal Mode hook ${name} failed`, error); } catch {}
59
+ }
60
+ };
61
+
47
62
  return {
48
- "command.execute.before": async (input, output) => {
63
+ "command.execute.before": safe("command.execute.before", async (input, output) => {
49
64
  const command = extractCommandName(input);
50
65
  if (command !== "goal") return;
51
66
 
52
67
  const response = commands.handleGoalCommand(extractCommandArguments(input));
53
68
  replaceGoalCommandPrompt(input, output, response.message);
54
- },
69
+ }),
55
70
 
56
- "experimental.chat.system.transform": async (_input, output) => {
71
+ "experimental.chat.system.transform": safe("experimental.chat.system.transform", async (_input, output) => {
57
72
  const block = supervisor.renderSystemContext();
58
73
  if (!block) return;
59
74
 
@@ -64,18 +79,18 @@ export function createOpenCodeGoalHooks(deps: OpenCodeGoalHooksDeps): OpenCodeGo
64
79
 
65
80
  const lastIndex = output.system.length - 1;
66
81
  output.system[lastIndex] = upsertMarkedBlock(output.system[lastIndex]!, block);
67
- },
82
+ }),
68
83
 
69
- "experimental.session.compacting": async (_input, output) => {
84
+ "experimental.session.compacting": safe("experimental.session.compacting", async (_input, output) => {
70
85
  const block = supervisor.renderSystemContext();
71
86
  if (!block) return;
72
87
 
73
88
  if (!output.context.some((entry) => entry.includes(GOAL_MODE_MARKER_START))) {
74
89
  output.context.push(block);
75
90
  }
76
- },
91
+ }),
77
92
 
78
- event: async ({ event }) => {
93
+ event: safe("event", async ({ event }) => {
79
94
  if (event.type !== "session.idle") return;
80
95
 
81
96
  const decision = supervisor.decide();
@@ -147,7 +162,7 @@ export function createOpenCodeGoalHooks(deps: OpenCodeGoalHooksDeps): OpenCodeGo
147
162
  } finally {
148
163
  autoContinueInFlight.delete(dedupeKey);
149
164
  }
150
- },
165
+ }),
151
166
  };
152
167
  }
153
168
 
@@ -4,7 +4,7 @@ import fs from "node:fs";
4
4
  import { execFileSync } from "node:child_process";
5
5
  import { createHash } from "node:crypto";
6
6
  import { createGoalStore } from "./goal/store.js";
7
- import { createOpenCodeGoalHooks } from "./goal/opencode-hooks.js";
7
+ import { createOpenCodeGoalHooks, type OpenCodeGoalHooks } from "./goal/opencode-hooks.js";
8
8
 
9
9
  interface GoalPluginLogger {
10
10
  warn?: (message: string, details?: unknown) => void;
@@ -51,11 +51,12 @@ function parseRemoteProjectKey(remote: string): string | undefined {
51
51
  return genericMatch?.[1];
52
52
  }
53
53
 
54
- export const GoalModePlugin = async (ctx: { directory: string; client?: unknown }) => {
54
+ export const GoalModePlugin = async (ctx: { directory: string; client?: unknown }): Promise<OpenCodeGoalHooks> => {
55
55
  const logger = createGoalPluginLogger(ctx.client);
56
- const databasePath = resolveGoalDatabasePath(process.env.JORGEX_GOAL_DB);
57
- const store = createGoalStore({ databasePath });
56
+ let store: ReturnType<typeof createGoalStore> | undefined;
58
57
  try {
58
+ const databasePath = resolveGoalDatabasePath(process.env.JORGEX_GOAL_DB);
59
+ store = createGoalStore({ databasePath });
59
60
  store.migrate();
60
61
  const project = resolveGoalProjectName(ctx.directory, logger);
61
62
 
@@ -67,20 +68,26 @@ export const GoalModePlugin = async (ctx: { directory: string; client?: unknown
67
68
  logger,
68
69
  });
69
70
  } catch (error) {
70
- store.close();
71
- throw error;
71
+ // If Goal Mode initialization fails (e.g., JORGEX_GOAL_DB points outside
72
+ // the allowed directory), log the error and return empty hooks. This
73
+ // prevents Goal Mode from crashing the entire plugin layer.
74
+ logger.error?.("Goal Mode failed to initialize; Goal hooks disabled.", error);
75
+ store?.close();
76
+ return {};
72
77
  }
73
78
  };
74
79
 
75
80
  function createGoalPluginLogger(client: unknown): GoalPluginLogger {
76
81
  return {
77
82
  warn: (message, details) => {
83
+ // Log to OpenCode only; console.warn is omitted to avoid log pollution
84
+ // in user terminals. All logs go through OpenCode's log stream.
78
85
  void logToOpenCode(client, "warn", message, details);
79
- console.warn(message, details);
80
86
  },
81
87
  error: (message, details) => {
88
+ // Log to OpenCode only; console.error is omitted to avoid log pollution
89
+ // in user terminals. All logs go through OpenCode's log stream.
82
90
  void logToOpenCode(client, "error", message, details);
83
- console.error(message, details);
84
91
  },
85
92
  };
86
93
  }
@@ -91,13 +98,14 @@ async function logToOpenCode(client: unknown, level: "warn" | "error", message:
91
98
  if (typeof app !== "object" || app === null) return;
92
99
  const log = (app as { log?: unknown }).log;
93
100
  if (typeof log !== "function") return;
101
+ const safeExtra = details instanceof Error ? { message: details.message, stack: details.stack } : details;
94
102
  try {
95
103
  await log.call(app, {
96
104
  body: {
97
105
  service: "goal-mode",
98
106
  level,
99
107
  message,
100
- extra: details,
108
+ extra: safeExtra,
101
109
  },
102
110
  });
103
111
  } catch {
@@ -358,7 +358,8 @@ export const WorktreePlugin: Plugin = async ({ $, client, directory }) => {
358
358
 
359
359
  const branchName = `${config.branchPrefix || "feature/"}${worktreeName}`;
360
360
 
361
- const gitRoot = await $`git rev-parse --show-toplevel`.text();
361
+ // .quiet() suppresses stderr to prevent noisy TUI logs from git calls
362
+ const gitRoot = await $`git rev-parse --show-toplevel`.quiet().text();
362
363
  const projectRoot = String(gitRoot).trim().replace(/\\/g, "/");
363
364
  const commandCwd = getCommandCwd(args, directory);
364
365
  const absoluteWorktreePath = resolvePath(commandCwd, parsedWorktreePath);