loki-mode 7.42.0 → 7.44.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.
@@ -1,5 +1,5 @@
1
1
  // @bun
2
- var n6=Object.defineProperty;var a6=($)=>$;function s6($,Q){this[$]=a6.bind(null,Q)}var h=($,Q)=>{for(var Z in Q)n6($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:s6.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var K$=import.meta.require;var S1={};h(S1,{lokiDir:()=>P,homeLokiDir:()=>o$,findRepoRootForVersion:()=>d$,REPO_ROOT:()=>m});import{resolve as n,dirname as l$}from"path";import{fileURLToPath as t6}from"url";import{existsSync as P$}from"fs";import{homedir as r6}from"os";function i6(){let $=N1;for(let Q=0;Q<6;Q++){if(P$(n($,"VERSION"))&&P$(n($,"autonomy/run.sh")))return $;let Z=l$($);if(Z===$)break;$=Z}return n(N1,"..","..","..")}function d$($){let Q=$;for(let Z=0;Z<6;Z++){if(P$(n(Q,"VERSION"))&&P$(n(Q,"autonomy/run.sh")))return Q;let z=l$(Q);if(z===Q)break;Q=z}return n($,"..","..","..")}function P(){return process.env.LOKI_DIR??n(process.cwd(),".loki")}function o$(){return n(r6(),".loki")}var N1,m;var C=L(()=>{N1=l$(t6(import.meta.url));m=i6()});import{readFileSync as e6}from"fs";import{resolve as $Q,dirname as QQ}from"path";import{fileURLToPath as ZQ}from"url";function F$(){if($$!==null)return $$;let $="7.42.0";if(typeof $==="string"&&$.length>0)return $$=$,$$;try{let Q=QQ(ZQ(import.meta.url)),Z=d$(Q);$$=e6($Q(Z,"VERSION"),"utf-8").trim()}catch{$$="unknown"}return $$}var $$=null;var n$=L(()=>{C()});var C1={};h(C1,{runOrThrow:()=>zQ,run:()=>j,commandVersion:()=>KQ,commandExists:()=>f,ShellError:()=>a$});async function j($,Q={}){let Z=Bun.spawn({cmd:[...$],stdout:"pipe",stderr:"pipe",env:Q.env?{...process.env,...Q.env}:process.env,cwd:Q.cwd}),z,X;if(Q.timeoutMs&&Q.timeoutMs>0)z=setTimeout(()=>{try{Z.kill("SIGTERM")}catch{}X=setTimeout(()=>{try{Z.kill("SIGKILL")}catch{}},2000)},Q.timeoutMs);try{let[W,K,U]=await Promise.all([new Response(Z.stdout).text(),new Response(Z.stderr).text(),Z.exited]);return{stdout:W,stderr:K,exitCode:U}}finally{if(z)clearTimeout(z);if(X)clearTimeout(X)}}async function zQ($,Q={}){let Z=await j($,Q);if(Z.exitCode!==0)throw new a$(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function f($){let Q=XQ($),Z=await j(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function XQ($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function KQ($,Q="--version"){if(!await f($))return null;let z=await j([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var a$;var d=L(()=>{a$=class a$ extends Error{message;exitCode;stdout;stderr;constructor($,Q,Z,z){super($);this.message=$;this.exitCode=Q;this.stdout=Z;this.stderr=z;this.name="ShellError"}}});function a($){return WQ?"":$}var WQ,T,S,I,TZ,w,R,y,q;var c=L(()=>{WQ=(process.env.NO_COLOR??"").length>0;T=a("\x1B[0;31m"),S=a("\x1B[0;32m"),I=a("\x1B[1;33m"),TZ=a("\x1B[0;34m"),w=a("\x1B[0;36m"),R=a("\x1B[1m"),y=a("\x1B[2m"),q=a("\x1B[0m")});import{existsSync as TQ}from"fs";async function Q$(){if(B$!==void 0)return B$;let $="/opt/homebrew/bin/python3.12";if(TQ($))return B$=$,$;let Q=await f("python3.12");if(Q)return B$=Q,Q;let Z=await f("python3");return B$=Z,Z}async function Z$($,Q={}){let Z=await Q$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return j([Z,"-c",$],Q)}var B$;var W$=L(()=>{d()});var t1={};h(t1,{runStatus:()=>gQ});import{existsSync as v,readFileSync as U$,readdirSync as l1,statSync as d1}from"fs";import{resolve as D,basename as xQ}from"path";import{homedir as NQ}from"os";async function DQ(){if(await f("jq"))return!0;return process.stdout.write(`${T}Error: jq is required but not installed.${q}
2
+ var n6=Object.defineProperty;var a6=($)=>$;function s6($,Q){this[$]=a6.bind(null,Q)}var h=($,Q)=>{for(var Z in Q)n6($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:s6.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var K$=import.meta.require;var S1={};h(S1,{lokiDir:()=>P,homeLokiDir:()=>o$,findRepoRootForVersion:()=>d$,REPO_ROOT:()=>m});import{resolve as n,dirname as l$}from"path";import{fileURLToPath as t6}from"url";import{existsSync as P$}from"fs";import{homedir as r6}from"os";function i6(){let $=N1;for(let Q=0;Q<6;Q++){if(P$(n($,"VERSION"))&&P$(n($,"autonomy/run.sh")))return $;let Z=l$($);if(Z===$)break;$=Z}return n(N1,"..","..","..")}function d$($){let Q=$;for(let Z=0;Z<6;Z++){if(P$(n(Q,"VERSION"))&&P$(n(Q,"autonomy/run.sh")))return Q;let z=l$(Q);if(z===Q)break;Q=z}return n($,"..","..","..")}function P(){return process.env.LOKI_DIR??n(process.cwd(),".loki")}function o$(){return n(r6(),".loki")}var N1,m;var C=L(()=>{N1=l$(t6(import.meta.url));m=i6()});import{readFileSync as e6}from"fs";import{resolve as $Q,dirname as QQ}from"path";import{fileURLToPath as ZQ}from"url";function F$(){if($$!==null)return $$;let $="7.44.0";if(typeof $==="string"&&$.length>0)return $$=$,$$;try{let Q=QQ(ZQ(import.meta.url)),Z=d$(Q);$$=e6($Q(Z,"VERSION"),"utf-8").trim()}catch{$$="unknown"}return $$}var $$=null;var n$=L(()=>{C()});var C1={};h(C1,{runOrThrow:()=>zQ,run:()=>j,commandVersion:()=>KQ,commandExists:()=>f,ShellError:()=>a$});async function j($,Q={}){let Z=Bun.spawn({cmd:[...$],stdout:"pipe",stderr:"pipe",env:Q.env?{...process.env,...Q.env}:process.env,cwd:Q.cwd}),z,X;if(Q.timeoutMs&&Q.timeoutMs>0)z=setTimeout(()=>{try{Z.kill("SIGTERM")}catch{}X=setTimeout(()=>{try{Z.kill("SIGKILL")}catch{}},2000)},Q.timeoutMs);try{let[W,K,U]=await Promise.all([new Response(Z.stdout).text(),new Response(Z.stderr).text(),Z.exited]);return{stdout:W,stderr:K,exitCode:U}}finally{if(z)clearTimeout(z);if(X)clearTimeout(X)}}async function zQ($,Q={}){let Z=await j($,Q);if(Z.exitCode!==0)throw new a$(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function f($){let Q=XQ($),Z=await j(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function XQ($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function KQ($,Q="--version"){if(!await f($))return null;let z=await j([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var a$;var d=L(()=>{a$=class a$ extends Error{message;exitCode;stdout;stderr;constructor($,Q,Z,z){super($);this.message=$;this.exitCode=Q;this.stdout=Z;this.stderr=z;this.name="ShellError"}}});function a($){return WQ?"":$}var WQ,T,S,I,TZ,w,R,y,q;var c=L(()=>{WQ=(process.env.NO_COLOR??"").length>0;T=a("\x1B[0;31m"),S=a("\x1B[0;32m"),I=a("\x1B[1;33m"),TZ=a("\x1B[0;34m"),w=a("\x1B[0;36m"),R=a("\x1B[1m"),y=a("\x1B[2m"),q=a("\x1B[0m")});import{existsSync as TQ}from"fs";async function Q$(){if(B$!==void 0)return B$;let $="/opt/homebrew/bin/python3.12";if(TQ($))return B$=$,$;let Q=await f("python3.12");if(Q)return B$=Q,Q;let Z=await f("python3");return B$=Z,Z}async function Z$($,Q={}){let Z=await Q$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return j([Z,"-c",$],Q)}var B$;var W$=L(()=>{d()});var t1={};h(t1,{runStatus:()=>gQ});import{existsSync as v,readFileSync as U$,readdirSync as l1,statSync as d1}from"fs";import{resolve as D,basename as xQ}from"path";import{homedir as NQ}from"os";async function DQ(){if(await f("jq"))return!0;return process.stdout.write(`${T}Error: jq is required but not installed.${q}
3
3
  `),process.stdout.write(`Install with:
4
4
  `),process.stdout.write(` brew install jq (macOS)
5
5
  `),process.stdout.write(` apt install jq (Debian/Ubuntu)
@@ -789,4 +789,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
789
789
  `),2}default:return process.stderr.write(`Unknown command: ${Q}
790
790
  `),process.stderr.write(o6),2}}p1();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var ZZ=await QZ(Bun.argv.slice(2));process.exit(ZZ);
791
791
 
792
- //# debugId=D7F92E946CD3E45564756E2164756E21
792
+ //# debugId=EAB4A25E0B3FC92864756E2164756E21
package/mcp/__init__.py CHANGED
@@ -57,4 +57,4 @@ try:
57
57
  except ImportError:
58
58
  __all__ = ['mcp']
59
59
 
60
- __version__ = '7.42.0'
60
+ __version__ = '7.44.0'
package/mcp/server.py CHANGED
@@ -1257,6 +1257,12 @@ async def get_continuity() -> str:
1257
1257
  return "# CONTINUITY.md not found"
1258
1258
  except PathTraversalError:
1259
1259
  return "# Access denied"
1260
+ except Exception as e:
1261
+ # Match the tool-handler error-envelope pattern so a corrupt or
1262
+ # unreadable state file (e.g. IsADirectoryError, OSError) returns an
1263
+ # honest error string instead of raising uncaught into the MCP runtime.
1264
+ logger.error(f"get_continuity failed: {e}")
1265
+ return f"# Error reading CONTINUITY.md: {e}"
1260
1266
 
1261
1267
 
1262
1268
  @mcp.resource("loki://memory/index")
@@ -1271,14 +1277,26 @@ async def get_memory_index() -> str:
1271
1277
  return json.dumps(index_data)
1272
1278
  return json.dumps({"topics": [], "message": "Index not initialized"})
1273
1279
 
1274
- # Fallback to direct file read
1280
+ # Fallback to direct file read. Parse-and-reserialize so a corrupt
1281
+ # index.json yields a clean error envelope instead of serving corrupt
1282
+ # bytes as a successful response (or raising on a downstream consumer).
1275
1283
  index_path = safe_path_join('.loki', 'memory', 'index.json')
1276
1284
  if os.path.exists(index_path):
1277
1285
  with safe_open(index_path, 'r') as f:
1278
- return f.read()
1286
+ raw = f.read()
1287
+ try:
1288
+ return json.dumps(json.loads(raw))
1289
+ except (json.JSONDecodeError, ValueError) as e:
1290
+ logger.error(f"get_memory_index: corrupt index.json: {e}")
1291
+ return json.dumps({"error": f"corrupt index.json: {e}", "topics": []})
1279
1292
  return json.dumps({"topics": [], "message": "Index not initialized"})
1280
1293
  except PathTraversalError:
1281
1294
  return json.dumps({"error": "Access denied", "topics": []})
1295
+ except Exception as e:
1296
+ # Generic envelope so any other state-file failure (OSError,
1297
+ # IsADirectoryError) returns honestly rather than raising uncaught.
1298
+ logger.error(f"get_memory_index failed: {e}")
1299
+ return json.dumps({"error": str(e), "topics": []})
1282
1300
 
1283
1301
 
1284
1302
  @mcp.resource("loki://queue/pending")
@@ -1304,6 +1322,12 @@ async def get_pending_tasks() -> str:
1304
1322
  return json.dumps({"pending_tasks": [], "count": 0})
1305
1323
  except PathTraversalError:
1306
1324
  return json.dumps({"error": "Access denied", "pending_tasks": [], "count": 0})
1325
+ except Exception as e:
1326
+ # Generic envelope: a degraded install (STATE_MANAGER_AVAILABLE=False)
1327
+ # does a bare json.load on .loki/state/task-queue.json; a corrupt file
1328
+ # raises JSONDecodeError that must return an error, not crash the runtime.
1329
+ logger.error(f"get_pending_tasks failed: {e}")
1330
+ return json.dumps({"error": str(e), "pending_tasks": [], "count": 0})
1307
1331
 
1308
1332
 
1309
1333
  # ============================================================
@@ -281,7 +281,12 @@ class VectorIndex:
281
281
  import tempfile
282
282
  npz_path = f"{path}.npz"
283
283
  npz_dir = os.path.dirname(npz_path) or "."
284
- tmp_fd, tmp_path = tempfile.mkstemp(dir=npz_dir, suffix=".npz.tmp")
284
+ # The temp file MUST end in ".npz". np.savez appends ".npz" to any
285
+ # target whose name does not already end in ".npz", so a ".npz.tmp"
286
+ # suffix would make numpy write the real archive to <tmp>.npz and leave
287
+ # the original temp file 0 bytes. os.replace would then move the empty
288
+ # file into place and orphan the real data (corrupting every index).
289
+ tmp_fd, tmp_path = tempfile.mkstemp(dir=npz_dir, suffix=".npz")
285
290
  os.close(tmp_fd)
286
291
  try:
287
292
  np.savez(
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "loki-mode",
3
3
  "mcpName": "io.github.asklokesh/loki-mode",
4
- "version": "7.42.0",
4
+ "version": "7.44.0",
5
5
  "description": "Loki Mode by Autonomi. Autonomous spec-to-product system: takes a PRD, GitHub issue, OpenAPI/JSON/YAML, or one-line brief to a deployed app via the RARV-C closure loop with 11 quality gates. Provider-agnostic (Claude Code, OpenAI Codex, Cline, Aider).",
6
6
  "keywords": [
7
7
  "agent",
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "name": "loki-mode",
4
4
  "displayName": "Loki Mode",
5
- "version": "7.42.0",
5
+ "version": "7.44.0",
6
6
  "description": "Autonomous spec-to-product build system with a built-in trust layer (RARV-C closure loop, 11 quality gates, completion council). Ships Loki's spec-hardening, drift-detection, and deterministic PR verification commands plus the Loki MCP server.",
7
7
  "author": {
8
8
  "name": "Autonomi",
@@ -116,10 +116,17 @@ provider_version() {
116
116
  # Invocation function
117
117
  # Note: Codex uses positional prompt, not -p flag
118
118
  # Note: Reasoning effort is configured via environment or config, not CLI flag
119
+ # v7.x: pin the resolved model explicitly via -m/--model. Without it, codex
120
+ # falls back to the installed CLI's built-in default (e.g. gpt-5.5 on codex
121
+ # 0.132.0), which silently ignores _codex_validate_model and makes the run.sh
122
+ # cost table (priced for gpt-5.3-codex) wrong. --model is the documented model
123
+ # selector and is readable in process listings.
119
124
  provider_invoke() {
120
125
  local prompt="$1"
121
126
  shift
122
- codex exec --full-auto --skip-git-repo-check "$prompt" "$@"
127
+ codex exec --full-auto --skip-git-repo-check \
128
+ --model "$PROVIDER_MODEL_DEVELOPMENT" \
129
+ "$prompt" "$@"
123
130
  }
124
131
 
125
132
  # Model tier to effort level parameter (Codex uses effort, not separate models)
@@ -197,6 +204,18 @@ provider_invoke_with_tier() {
197
204
  local effort
198
205
  effort=$(resolve_model_for_tier "$tier")
199
206
 
207
+ # Resolve the model name by tier. These three vars can diverge via the
208
+ # generic LOKI_MODEL_* env (each validated by _codex_validate_model), so
209
+ # honor the tier rather than hardcoding development. Capability aliases
210
+ # (best/balanced/cheap) mirror resolve_model_for_tier's mapping.
211
+ local model
212
+ case "$tier" in
213
+ planning|best) model="$PROVIDER_MODEL_PLANNING" ;;
214
+ development|balanced) model="$PROVIDER_MODEL_DEVELOPMENT" ;;
215
+ fast|cheap) model="$PROVIDER_MODEL_FAST" ;;
216
+ *) model="$PROVIDER_MODEL_DEVELOPMENT" ;;
217
+ esac
218
+
200
219
  local extra_flags=()
201
220
  if [ "${LOKI_CODEX_WEB_SEARCH:-false}" = "true" ]; then
202
221
  extra_flags+=(--search)
@@ -211,6 +230,7 @@ provider_invoke_with_tier() {
211
230
  --ask-for-approval never \
212
231
  --sandbox danger-full-access \
213
232
  --skip-git-repo-check \
233
+ --model "$model" \
214
234
  "${extra_flags[@]}" \
215
235
  "$prompt" "$@"
216
236
  }