mindweave 2.4.8 → 2.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.
Files changed (72) hide show
  1. package/README.md +4 -2
  2. package/dist/cli/App.js +101 -27
  3. package/dist/cli/App.js.map +1 -1
  4. package/dist/cli/commandArgs.js +66 -16
  5. package/dist/cli/commandArgs.js.map +1 -1
  6. package/dist/cli/commands.js +1 -0
  7. package/dist/cli/commands.js.map +1 -1
  8. package/dist/cli/components/ApprovalBox.js +10 -1
  9. package/dist/cli/components/ApprovalBox.js.map +1 -1
  10. package/dist/cli/components/McpMinitabs.js +1 -1
  11. package/dist/cli/components/Picker.js +61 -19
  12. package/dist/cli/components/Picker.js.map +1 -1
  13. package/dist/cli/components/ToolLine.js +1 -1
  14. package/dist/cli/components/ToolLine.js.map +1 -1
  15. package/dist/cli/feedback.js +223 -0
  16. package/dist/cli/feedback.js.map +1 -0
  17. package/dist/cli/framebuffer/writer.js +1 -1
  18. package/dist/cli/toolDisplay.js +4 -3
  19. package/dist/cli/toolDisplay.js.map +1 -1
  20. package/dist/drivers/clientId.js +2 -3
  21. package/dist/drivers/clientId.js.map +1 -1
  22. package/dist/drivers/openaiCompat/wire.js +55 -3
  23. package/dist/drivers/openaiCompat/wire.js.map +1 -1
  24. package/dist/drivers/openrouter/catalog.js +136 -0
  25. package/dist/drivers/openrouter/catalog.js.map +1 -0
  26. package/dist/drivers/openrouter/client.js +102 -0
  27. package/dist/drivers/openrouter/client.js.map +1 -0
  28. package/dist/drivers/openrouter/endpoint.js +20 -0
  29. package/dist/drivers/openrouter/endpoint.js.map +1 -0
  30. package/dist/drivers/openrouter/index.js +5 -0
  31. package/dist/drivers/openrouter/index.js.map +1 -0
  32. package/dist/drivers/openrouter/manifest.js +83 -0
  33. package/dist/drivers/openrouter/manifest.js.map +1 -0
  34. package/dist/drivers/providerError.js +51 -0
  35. package/dist/drivers/providerError.js.map +1 -1
  36. package/dist/drivers/registry.js +111 -3
  37. package/dist/drivers/registry.js.map +1 -1
  38. package/dist/dynamo/engine.js +152 -99
  39. package/dist/dynamo/engine.js.map +1 -1
  40. package/dist/dynamo/model.js +63 -4
  41. package/dist/dynamo/model.js.map +1 -1
  42. package/dist/governor/skills.js +1 -1
  43. package/dist/governor/write.js +61 -0
  44. package/dist/governor/write.js.map +1 -1
  45. package/dist/mcp/configWrite.js +1 -1
  46. package/dist/memory/compaction.js +9 -1
  47. package/dist/memory/compaction.js.map +1 -1
  48. package/dist/memory/store.js +8 -1
  49. package/dist/memory/store.js.map +1 -1
  50. package/dist/tools/backgroundShells.js +4 -3
  51. package/dist/tools/backgroundShells.js.map +1 -1
  52. package/dist/tools/deferredNative.js +13 -4
  53. package/dist/tools/deferredNative.js.map +1 -1
  54. package/dist/tools/governorTools.js +113 -9
  55. package/dist/tools/governorTools.js.map +1 -1
  56. package/dist/tools/mcpAdd.js +87 -4
  57. package/dist/tools/mcpAdd.js.map +1 -1
  58. package/dist/tools/mcpSearch.js +5 -4
  59. package/dist/tools/mcpSearch.js.map +1 -1
  60. package/dist/tools/mindweaveStatus.js +113 -0
  61. package/dist/tools/mindweaveStatus.js.map +1 -0
  62. package/dist/tools/nativeStderr.js +27 -0
  63. package/dist/tools/nativeStderr.js.map +1 -0
  64. package/dist/tools/registry.js +6 -4
  65. package/dist/tools/registry.js.map +1 -1
  66. package/dist/tools/runCommand.js +53 -18
  67. package/dist/tools/runCommand.js.map +1 -1
  68. package/dist/tools/screenshot.js +17 -4
  69. package/dist/tools/screenshot.js.map +1 -1
  70. package/dist/tools/todo.js +5 -5
  71. package/dist/tools/todo.js.map +1 -1
  72. package/package.json +4 -3
package/README.md CHANGED
@@ -70,8 +70,9 @@ else is configured inside a session.
70
70
 
71
71
  Short version, one line each. The depth is in the linked pages.
72
72
 
73
- - **14 providers, 52 models, one key** — DeepSeek, Anthropic, OpenAI, Gemini, xAI,
74
- Mistral, Groq, Cerebras, Qwen, Kimi, GLM, Meta, MiniMax, Tencent. Only the driver you
73
+ - **15 providers, 53 models, one key** — DeepSeek, Anthropic, OpenAI, Gemini, xAI,
74
+ Mistral, Groq, Cerebras, Qwen, Kimi, GLM, Meta, MiniMax, Tencent, plus every
75
+ tool-capable model on OpenRouter. Only the driver you
75
76
  use is loaded. Switch with `/provider` and `/model`; remembered per project.
76
77
  [PROVIDERS.md](src/drivers/PROVIDERS.md)
77
78
  - **Real tools** — read and edit files, multi-file edits, ripgrep search, a shell with
@@ -105,6 +106,7 @@ Short version, one line each. The depth is in the linked pages.
105
106
  | `/undo` | Reverts what the last turn changed |
106
107
  | `/update` | Installs the newest version and reopens on this conversation |
107
108
  | `/mcp` | Manages connected MCP servers |
109
+ | `/feedback` | Sends the maintainer a message, no account needed |
108
110
  | `shift-tab` | Cycles interaction modes |
109
111
 
110
112
  Type while it works and your message queues; press up to take it back and edit it.
package/dist/cli/App.js CHANGED
@@ -35,7 +35,8 @@ import { appendForbidden, appendForbiddenCommand } from "../governor/write.js";
35
35
  import { addRoot, removeRoot } from "../tools/workspace.js";
36
36
  import { discoverRelatedRoots } from "../tools/workspaceDiscover.js";
37
37
  import { rootLabel, rootsOf, relativize } from "../tools/paths.js";
38
- import { APPROVAL_DISMISSED, APPROVAL_TEXT } from "../tools/approval.js";
38
+ import { APPROVAL_DISMISSED, APPROVAL_TEXT, readFreeText } from "../tools/approval.js";
39
+ import { buildFeedback, previewOf, refuseReason, sendFeedback, withAddition } from "./feedback.js";
39
40
  import { KeySetup } from "./components/KeySetup.js";
40
41
  import { setupView } from "./keySetup.js";
41
42
  import { KeyManager } from "./components/KeyManager.js";
@@ -46,10 +47,10 @@ import { TrustGate } from "./components/TrustGate.js";
46
47
  import { rootBreadth, breadthWarning, trustPersists, isTrusted, rememberTrust } from "./trust.js";
47
48
  import { projectDir } from "../memory/store.js";
48
49
  import { parseUndoArg, undoNotice } from "../tools/checkpoints.js";
49
- import { DEFAULT_MODEL_CONFIG, thinkLevels, thinkLabel, modelLabel, modelsOfProvider, providerOf, usableFallback, needsKeySetup, withModel, saveModelConfig, refreshModels } from "../dynamo/model.js";
50
+ import { DEFAULT_MODEL_CONFIG, thinkLevels, thinkLabel, modelLabel, modelsOfProvider, providerOf, usableFallback, needsKeySetup, withModel, saveModelConfig, refreshModels, DISCOVERY_TTL_MS } from "../dynamo/model.js";
50
51
  import { allProviders, manifestForModel, modelsOf } from "../drivers/registry.js";
51
52
  import { orderProviders, orderModels } from "./pickerOrder.js";
52
- import { accessRefusal } from "../drivers/providerError.js";
53
+ import { accessRefusal, providerOutage } from "../drivers/providerError.js";
53
54
  import { resolveAttachments, stripAttachments } from "./attachments.js";
54
55
  import { collapsePastes, wrapPastedText } from "../memory/pastedText.js";
55
56
  import { createDropHandles, expandHandles } from "./dropHandles.js";
@@ -89,7 +90,7 @@ import { perf, perfEnabled } from "./perfLog.js";
89
90
  import { isGroupMember, groupSettled, planGroupReveal, planStandaloneReveal, resultQueued, STANDALONE_HOLD_MS } from "./groupReveal.js";
90
91
  import { drain as drainQueue, popAll as popAllQueued, queueMessage, takeSteerable, visibleQueue } from "./messageQueue.js";
91
92
  import { routeCommand, parseCommandLine, unknownCommandMessage } from "./commandRoute.js";
92
- import { resolveChoice } from "./commandArgs.js";
93
+ import { resolveChoice, splitModelArg } from "./commandArgs.js";
93
94
  import { carryAcrossFreshSession } from "./sessionCarry.js";
94
95
  import { toolDisplay, isGroupable, KIND_COLOR } from "./toolDisplay.js";
95
96
  import { workingVerb } from "./workingVerb.js";
@@ -141,6 +142,11 @@ function orderedProviderList() {
141
142
  function orderedModelList(model) {
142
143
  return orderModels(modelsOfProvider(model));
143
144
  }
145
+ /** The same order, for a provider named by id rather than by one of its models. */
146
+ function orderedModelsOf(providerId) {
147
+ const provider = allProviders().find((p) => p.id === providerId);
148
+ return provider ? orderModels(modelsOf(provider)) : [];
149
+ }
144
150
  // After you pick a session in /continue, the three ways to resume it.
145
151
  const RESUME_MODES = [
146
152
  { label: "Compact & continue", description: "summarize the old chat first so it won't eat your context, then pick up where you left off" },
@@ -1427,7 +1433,7 @@ export function App({ resumeSessionId, initialScreen }) {
1427
1433
  /**
1428
1434
  * Write a server's config and connect it live — shared by the typed `/mcp add`, the
1429
1435
  * `/mcp` box's Add and Edit, and (indirectly, through the same writer) the
1430
- * `add_mcp_server` tool. One path so a config any of them accepts is one the others
1436
+ * `mcp_server` tool. One path so a config any of them accepts is one the others
1431
1437
  * would too. Connects immediately: writing the file and telling the user to restart
1432
1438
  * would defeat the entire point of a guided add.
1433
1439
  */
@@ -1476,7 +1482,7 @@ export function App({ resumeSessionId, initialScreen }) {
1476
1482
  /**
1477
1483
  * `/mcp add <name> <command|url> [args…]` and `/mcp remove <name>`.
1478
1484
  *
1479
- * Shares its parser and writer with the `add_mcp_server` tool, so a config the command
1485
+ * Shares its parser and writer with the `mcp_server` tool, so a config the command
1480
1486
  * accepts is exactly one the tool would, and vice versa.
1481
1487
  */
1482
1488
  async function mcpConfigCommand(arg) {
@@ -2013,7 +2019,10 @@ export function App({ resumeSessionId, initialScreen }) {
2013
2019
  // the provider's own sentence quoted inside it. Anything else — a malformed
2014
2020
  // request, a bug of ours — stays loud, which is the point of classifying
2015
2021
  // narrowly. See drivers/providerError.ts.
2016
- const refusal = accessRefusal(error, providerOf(s.modelConfig.model).label, otherProviderHasKey(s.modelConfig.model));
2022
+ // A provider falling over after the retries ran out gets the same calm treatment:
2023
+ // it is not our crash, and the useful news is "wait, or switch model".
2024
+ const refusal = accessRefusal(error, providerOf(s.modelConfig.model).label, otherProviderHasKey(s.modelConfig.model)) ??
2025
+ providerOutage(error, providerOf(s.modelConfig.model).label, modelLabel(s.modelConfig.model));
2017
2026
  if (refusal)
2018
2027
  enqueueReveal({ type: "notice", title: refusal.title, body: refusal.body });
2019
2028
  else
@@ -2148,15 +2157,28 @@ export function App({ resumeSessionId, initialScreen }) {
2148
2157
  }
2149
2158
  // Apply a /model pick: switch model (clamping reasoning to a valid level), persist
2150
2159
  // the choice for this project, and confirm.
2151
- async function applyModel(index) {
2160
+ async function applyModel(index, providerId) {
2152
2161
  const s = session.current;
2153
- // Index into the SAME list the picker rendered — the current provider's models in
2154
- // display order, not every model everywhere. Indexing a differently-ordered list
2155
- // here would silently select a different model than the one on screen, and would
2156
- // type-check perfectly.
2157
- const choice = s ? orderedModelList(s.modelConfig.model)[index] : undefined;
2162
+ // Index into the SAME list the picker rendered — that provider's models in display
2163
+ // order, not every model everywhere. Indexing a differently-ordered list here would
2164
+ // silently select a different model than the one on screen, and would type-check
2165
+ // perfectly.
2166
+ const list = !s ? [] : providerId ? orderedModelsOf(providerId) : orderedModelList(s.modelConfig.model);
2167
+ const choice = list[index];
2158
2168
  if (!s || !choice)
2159
2169
  return;
2170
+ // A model on another provider is a provider switch, and gets the same key check.
2171
+ const target = manifestForModel(choice.id);
2172
+ if (target.id !== providerOf(s.modelConfig.model).id) {
2173
+ if (missingKeyFor(choice.id)) {
2174
+ pendingSwitch.current = { model: choice.id, apiKeyEnv: target.apiKeyEnv, label: target.label };
2175
+ note(`${target.label} has no key yet — add one and the switch will finish.`);
2176
+ setKeysOpen(true);
2177
+ return;
2178
+ }
2179
+ await switchTo(choice.id, target.label);
2180
+ return;
2181
+ }
2160
2182
  s.modelConfig = withModel(s.modelConfig, choice.id);
2161
2183
  await saveModelConfig(s.cwd, s.modelConfig);
2162
2184
  note(`model → ${modelLabel(s.modelConfig.model)} · ${thinkLabel(s.modelConfig)}`);
@@ -2202,6 +2224,10 @@ export function App({ resumeSessionId, initialScreen }) {
2202
2224
  s.modelConfig = withModel(s.modelConfig, model);
2203
2225
  await saveModelConfig(s.cwd, s.modelConfig);
2204
2226
  note(`provider → ${providerLabel} · model → ${modelLabel(s.modelConfig.model)} · ${thinkLabel(s.modelConfig)}`);
2227
+ // Where prompts go is worth a sentence at the moment someone moves their work there.
2228
+ const notice = manifestForModel(model).notice;
2229
+ if (notice)
2230
+ note(notice);
2205
2231
  }
2206
2232
  // Apply a /think pick for the current model: set thinking + effort, persist, confirm.
2207
2233
  async function applyThink(index) {
@@ -2256,7 +2282,7 @@ export function App({ resumeSessionId, initialScreen }) {
2256
2282
  else if (o.kind === "provider")
2257
2283
  void applyProvider(index);
2258
2284
  else if (o.kind === "model")
2259
- void applyModel(index);
2285
+ void applyModel(index, o.providerId);
2260
2286
  else if (o.kind === "think")
2261
2287
  void applyThink(index);
2262
2288
  else if (o.kind === "screen") {
@@ -2295,7 +2321,7 @@ export function App({ resumeSessionId, initialScreen }) {
2295
2321
  }
2296
2322
  // Hand a free-text directive to the model with an instruction wrapper, as its own
2297
2323
  // turn — used by the manual /rules and /skills commands (the model does the
2298
- // "rewrite it well and save it" work via remember_rule / create_skill).
2324
+ // "rewrite it well and save it" work via remember_rule / the skill tool).
2299
2325
  async function runDirective(instruction, activity) {
2300
2326
  const s = session.current;
2301
2327
  if (!s)
@@ -2397,6 +2423,41 @@ export function App({ resumeSessionId, initialScreen }) {
2397
2423
  }
2398
2424
  // /analytics — the anonymous usage ping. Bare opens the on/off switch, the same
2399
2425
  // fixed box every other setting uses; an argument acts directly, same as /model.
2426
+ // /feedback — a message to the maintainer, from inside the app. No account, no issue
2427
+ // tracker, no mail client: the whole point is that it costs the sender nothing. What
2428
+ // leaves the machine is decided in cli/feedback.ts and shown here before it goes.
2429
+ if (name === "/feedback") {
2430
+ const typed = arg.trim();
2431
+ const message = typed || (await askApproval.current("What would you like to tell the maintainer?", ["Cancel"], undefined, undefined, { label: "Your message", placeholder: "what is missing, broken, or worth adding" }).then((answer) => readFreeText(answer) ?? ""));
2432
+ if (!message.trim())
2433
+ return;
2434
+ // The confirm LOOPS, because the moment you read your own message back is the
2435
+ // moment you remember the thing you left out — usually an email address to reply
2436
+ // to. Typing in the box's own text row adds a line and shows the message again,
2437
+ // rather than cancelling and retyping the whole thing.
2438
+ let text = message;
2439
+ for (;;) {
2440
+ const refusal = refuseReason(text);
2441
+ if (refusal)
2442
+ return say(refusal);
2443
+ const feedback = buildFeedback(text, appVersion());
2444
+ const choice = await askApproval.current(previewOf(feedback), ["Send it", "Don't send"], undefined, undefined, {
2445
+ label: "add a line",
2446
+ placeholder: "your email for a reply, or more detail",
2447
+ });
2448
+ const added = readFreeText(choice);
2449
+ if (added !== null) {
2450
+ text = withAddition(text, added);
2451
+ continue;
2452
+ }
2453
+ if (choice !== "Send it")
2454
+ return note("Not sent.");
2455
+ note("sending…");
2456
+ const result = await sendFeedback(feedback);
2457
+ note(result.message, result.ok ? undefined : { error: true });
2458
+ return;
2459
+ }
2460
+ }
2400
2461
  if (name === "/analytics") {
2401
2462
  const verb = arg.trim().toLowerCase();
2402
2463
  if (verb === "on" || verb === "off") {
@@ -2620,12 +2681,12 @@ export function App({ resumeSessionId, initialScreen }) {
2620
2681
  setOverlay({ kind: "sessions", items: sessions });
2621
2682
  return;
2622
2683
  }
2623
- // Both pickers refresh discovered providers first, so a model pulled since the
2624
- // session started appears without a restart. Awaited rather than fired off: the
2684
+ // Both pickers refresh discovered providers whose list has gone stale, so a newly
2685
+ // served model appears without a restart. Awaited rather than fired off: the
2625
2686
  // picker renders from the list, and opening on a stale one that then changes
2626
2687
  // under the cursor is the exact "two-stage reveal" the UI work removed.
2627
2688
  if (name === "/provider") {
2628
- await refreshModels();
2689
+ await refreshModels({ maxAgeMs: DISCOVERY_TTL_MS });
2629
2690
  setOverlay({ kind: "provider" });
2630
2691
  return;
2631
2692
  }
@@ -2641,12 +2702,21 @@ export function App({ resumeSessionId, initialScreen }) {
2641
2702
  // getting the picker anyway — which is what happened before, because the argument
2642
2703
  // was dropped without a word — reads as the app not having heard you.
2643
2704
  if (name === "/model") {
2644
- await refreshModels();
2705
+ await refreshModels({ maxAgeMs: DISCOVERY_TTL_MS });
2645
2706
  if (arg) {
2646
- const picked = resolveChoice(arg, orderedModelList(s.modelConfig.model), "model");
2707
+ const current = providerOf(s.modelConfig.model).id;
2708
+ const here = resolveChoice(arg, orderedModelList(s.modelConfig.model), "model");
2709
+ const { providerId, words } = splitModelArg(arg, allProviders(), current, here.kind !== "error");
2710
+ // `/model openrouter` alone: that provider's picker.
2711
+ if (!words)
2712
+ return setOverlay({ kind: "model", providerId });
2713
+ const picked = providerId === current && words === arg.trim() ? here : resolveChoice(words, orderedModelsOf(providerId), "model");
2647
2714
  if (picked.kind === "error")
2648
2715
  return say(picked.message);
2649
- await applyModel(picked.index);
2716
+ // Several fit: show exactly those, in the picker, rather than a list to retype from.
2717
+ if (picked.kind === "several")
2718
+ return setOverlay({ kind: "model", providerId, filter: words });
2719
+ await applyModel(picked.index, providerId);
2650
2720
  return;
2651
2721
  }
2652
2722
  setOverlay({ kind: "model" });
@@ -2675,7 +2745,7 @@ export function App({ resumeSessionId, initialScreen }) {
2675
2745
  if (name === "/think") {
2676
2746
  if (arg) {
2677
2747
  const picked = resolveChoice(arg, thinkLevels(s.modelConfig.model), "reasoning level");
2678
- if (picked.kind === "error")
2748
+ if (picked.kind !== "match")
2679
2749
  return say(picked.message);
2680
2750
  await applyThink(picked.index);
2681
2751
  return;
@@ -2828,7 +2898,7 @@ export function App({ resumeSessionId, initialScreen }) {
2828
2898
  if (name === "/skills") {
2829
2899
  if (arg) {
2830
2900
  await runDirective(`The user wants a new reusable skill for this project: "${arg}". Design it and save it ` +
2831
- `with create_skill — a short invocation name, a one-line description, and the procedure ` +
2901
+ `with the skill tool — a short invocation name, a one-line description, and the procedure ` +
2832
2902
  `as a clear markdown checklist (use $ARGUMENTS/$1 if it should take input). Then confirm.`, "creating a skill…");
2833
2903
  return;
2834
2904
  }
@@ -3102,9 +3172,10 @@ export function App({ resumeSessionId, initialScreen }) {
3102
3172
  }
3103
3173
  if (overlay.kind === "model") {
3104
3174
  const id = cur?.modelConfig.model ?? DEFAULT_MODEL_CONFIG.model;
3105
- // Only the current provider's models, in display order (default first, rest A→Z).
3106
- // Switching provider is /provider's job.
3107
- const models = orderedModelList(id);
3175
+ // One provider's models, in display order (default first, rest A→Z): the one in use,
3176
+ // unless `/model <provider> …` named another.
3177
+ const models = overlay.providerId ? orderedModelsOf(overlay.providerId) : orderedModelList(id);
3178
+ const pickerProvider = overlay.providerId ? allProviders().find((p) => p.id === overlay.providerId)?.label ?? "" : providerOf(id).label;
3108
3179
  // The two facts that decide the choice and are nowhere else on the screen: how much
3109
3180
  // it can hold, and whether it can see an image you attach. They lead the description
3110
3181
  // because the row truncates from the RIGHT — put behind the prose they would be the
@@ -3116,9 +3187,12 @@ export function App({ resumeSessionId, initialScreen }) {
3116
3187
  return {
3117
3188
  label: m.label + (m.id === id ? " ✓" : ""),
3118
3189
  description: m.description ? `${facts} · ${m.description}` : facts,
3190
+ // Searched but not shown, so a filter opened by `/model <words>` matches the same
3191
+ // things the words matched: the id as well as the name.
3192
+ keywords: m.id,
3119
3193
  };
3120
3194
  });
3121
- return (_jsx(Picker, { title: `Choose a ${providerOf(id).label} model`, items: items, width: width, maxRows: maxRows, initialIndex: Math.max(0, models.findIndex((m) => m.id === id)), onSelect: onOverlaySelect, onCancel: onOverlayCancel }));
3195
+ return (_jsx(Picker, { title: `Choose ${/^[aeiou]/i.test(pickerProvider) ? "an" : "a"} ${pickerProvider} model`, items: items, width: width, maxRows: maxRows, initialIndex: Math.max(0, models.findIndex((m) => m.id === id)), initialFilter: overlay.filter, onSelect: onOverlaySelect, onCancel: onOverlayCancel }));
3122
3196
  }
3123
3197
  if (overlay.kind === "think") {
3124
3198
  const model = cur?.modelConfig.model ?? DEFAULT_MODEL_CONFIG.model;