@caupulican/pi-adaptative 0.80.93 → 0.80.95

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 (33) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/core/models/fitness-store.d.ts +2 -0
  3. package/dist/core/models/fitness-store.d.ts.map +1 -1
  4. package/dist/core/models/fitness-store.js +10 -0
  5. package/dist/core/models/fitness-store.js.map +1 -1
  6. package/dist/core/models/local-registration.d.ts +18 -0
  7. package/dist/core/models/local-registration.d.ts.map +1 -0
  8. package/dist/core/models/local-registration.js +83 -0
  9. package/dist/core/models/local-registration.js.map +1 -0
  10. package/dist/core/models/local-runtime.d.ts +84 -0
  11. package/dist/core/models/local-runtime.d.ts.map +1 -0
  12. package/dist/core/models/local-runtime.js +219 -0
  13. package/dist/core/models/local-runtime.js.map +1 -0
  14. package/dist/core/models/model-ref.d.ts +19 -0
  15. package/dist/core/models/model-ref.d.ts.map +1 -0
  16. package/dist/core/models/model-ref.js +61 -0
  17. package/dist/core/models/model-ref.js.map +1 -0
  18. package/dist/core/slash-commands.d.ts.map +1 -1
  19. package/dist/core/slash-commands.js +4 -0
  20. package/dist/core/slash-commands.js.map +1 -1
  21. package/dist/modes/interactive/interactive-mode.d.ts +7 -0
  22. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  23. package/dist/modes/interactive/interactive-mode.js +192 -10
  24. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  25. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  26. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  27. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  28. package/examples/extensions/sandbox/package-lock.json +2 -2
  29. package/examples/extensions/sandbox/package.json +1 -1
  30. package/examples/extensions/with-deps/package-lock.json +2 -2
  31. package/examples/extensions/with-deps/package.json +1 -1
  32. package/npm-shrinkwrap.json +12 -12
  33. package/package.json +4 -4
@@ -22,6 +22,10 @@ import { configureHttpDispatcher, formatHttpIdleTimeoutMs } from "../../core/htt
22
22
  import { KeybindingsManager } from "../../core/keybindings.js";
23
23
  import { createCompactionSummaryMessage } from "../../core/messages.js";
24
24
  import { cliProviderAliases, defaultModelPerProvider, findExactModelReferenceMatch, resolveCliModel, resolveModelScope, } from "../../core/model-resolver.js";
25
+ import { FitnessStore } from "../../core/models/fitness-store.js";
26
+ import { registerLocalModel, unregisterLocalModel } from "../../core/models/local-registration.js";
27
+ import { OllamaRuntime } from "../../core/models/local-runtime.js";
28
+ import { normalizeModelSource } from "../../core/models/model-ref.js";
25
29
  import { DefaultPackageManager } from "../../core/package-manager.js";
26
30
  import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "../../core/provider-display-names.js";
27
31
  import { getPendingReloadBlockers } from "../../core/reload-blockers.js";
@@ -2478,6 +2482,11 @@ export class InteractiveMode {
2478
2482
  }
2479
2483
  return;
2480
2484
  }
2485
+ if (text === "/models" || text.startsWith("/models ")) {
2486
+ void this.handleModelsCommand(text.slice("/models".length).trim());
2487
+ this.editor.setText("");
2488
+ return;
2489
+ }
2481
2490
  if (text === "/context") {
2482
2491
  this.showStatus(this.session.formatContextCompositionDashboard());
2483
2492
  this.editor.setText("");
@@ -5129,6 +5138,172 @@ export class InteractiveMode {
5129
5138
  this.settingsManager.setAutoLearnSettings(preset, scope);
5130
5139
  this.updateAutoLearnFooter();
5131
5140
  }
5141
+ _localRuntime;
5142
+ get localRuntime() {
5143
+ this._localRuntime ??= new OllamaRuntime({ agentDir: getAgentDir() });
5144
+ return this._localRuntime;
5145
+ }
5146
+ /**
5147
+ * /models — USER-invoked local model lifecycle (never a model-invokable tool):
5148
+ * list/add/remove/stop per local-model-lifecycle-design.md. Removal is explicit-only with
5149
+ * full disclosure; a pasted install command is parsed for its ref, never executed.
5150
+ */
5151
+ async handleModelsCommand(argsText) {
5152
+ const [action = "list", ...rest] = argsText.split(/\s+/).filter(Boolean);
5153
+ try {
5154
+ if (action === "stop") {
5155
+ const stopped = this.localRuntime.stop();
5156
+ this.showStatus(stopped.stopped
5157
+ ? "Pi-managed local model server stopped (models remain installed)."
5158
+ : "No pi-managed server running (a system server, if any, is not pi's to stop).");
5159
+ return;
5160
+ }
5161
+ if (action === "add") {
5162
+ const rawRef = rest.join(" ");
5163
+ if (!rawRef) {
5164
+ this.showStatus("Usage: /models add <ollama-tag | hf.co/org/repo[:quant] | huggingface URL | pasted install command>");
5165
+ return;
5166
+ }
5167
+ const source = normalizeModelSource(rawRef);
5168
+ if (source.type === "rejected") {
5169
+ this.showStatus(`Not added: ${source.reason}`);
5170
+ return;
5171
+ }
5172
+ if (source.type === "api") {
5173
+ this.showStatus(`${source.ref} is an API model — nothing to install. Configure auth for the provider, then probe it with /fitness ${source.ref}.`);
5174
+ return;
5175
+ }
5176
+ await this.addLocalModel(source.pullRef);
5177
+ return;
5178
+ }
5179
+ if (action === "remove") {
5180
+ const ref = rest[0];
5181
+ const confirmed = rest[1] === "confirm";
5182
+ if (!ref) {
5183
+ this.showStatus("Usage: /models remove <ref> confirm");
5184
+ return;
5185
+ }
5186
+ await this.removeLocalModel(ref, confirmed);
5187
+ return;
5188
+ }
5189
+ await this.listLocalModels();
5190
+ }
5191
+ catch (error) {
5192
+ this.showError(error instanceof Error ? error.message : String(error));
5193
+ }
5194
+ }
5195
+ async ensureLocalServer() {
5196
+ const status = await this.localRuntime.detect();
5197
+ if (status.serverUp)
5198
+ return true;
5199
+ if (!status.binaryPath) {
5200
+ for (const line of this.localRuntime.installGuide())
5201
+ this.showStatus(line);
5202
+ return false;
5203
+ }
5204
+ this.showStatus(`Starting local model server (${status.binarySource} binary, owned storage: ${status.ownedModelsDir})…`);
5205
+ const started = await this.localRuntime.start();
5206
+ if (!started.started) {
5207
+ this.showStatus(`Could not start the local server: ${started.reason}`);
5208
+ return false;
5209
+ }
5210
+ return true;
5211
+ }
5212
+ async listLocalModels() {
5213
+ const status = await this.localRuntime.detect();
5214
+ if (!status.serverUp) {
5215
+ if (!status.binaryPath) {
5216
+ for (const line of this.localRuntime.installGuide())
5217
+ this.showStatus(line);
5218
+ return;
5219
+ }
5220
+ this.showStatus(`Local server not running (binary: ${status.binarySource}). /models add starts it on demand; /fitness probes registered models.`);
5221
+ return;
5222
+ }
5223
+ const models = await this.localRuntime.list();
5224
+ const fitness = FitnessStore.forAgentDir(getAgentDir()).getForHost();
5225
+ const lines = [
5226
+ `Local models (${status.managedByPi ? `pi-managed server, storage: ${status.ownedModelsDir}` : "system server — storage owned by the system daemon"}):`,
5227
+ ...(models.length === 0 ? [" (none installed — /models add <ref>)"] : []),
5228
+ ...models.map((model) => {
5229
+ const report = fitness.find((entry) => entry.model === `ollama/${model.name}`);
5230
+ const gb = (model.sizeBytes / 1e9).toFixed(2);
5231
+ const probe = report
5232
+ ? `probed ${report.at.slice(0, 10)}: digest ${report.report.digest?.succeeded ?? "?"}/${report.report.digest?.total ?? "?"}, tool-calls ${report.report.toolCall.succeeded}/${report.report.toolCall.total}${report.report.tokensPerSecond ? `, ~${report.report.tokensPerSecond} tok/s` : ""}`
5233
+ : `unprobed — run /fitness ollama/${model.name}`;
5234
+ return ` - ${model.name} (${gb} GB) · ${probe}`;
5235
+ }),
5236
+ "Commands: /models add <ref> · /models remove <ref> confirm · /models stop",
5237
+ ];
5238
+ for (const line of lines)
5239
+ this.showStatus(line);
5240
+ }
5241
+ async addLocalModel(pullRef) {
5242
+ if (!(await this.ensureLocalServer()))
5243
+ return;
5244
+ this.showStatus(`Pulling ${pullRef}… (weights land in the server's model storage)`);
5245
+ let lastShown = 0;
5246
+ const pulled = await this.localRuntime.pull(pullRef, (progress) => {
5247
+ const now = Date.now();
5248
+ if (now - lastShown > 2000) {
5249
+ lastShown = now;
5250
+ this.showStatus(` ${pullRef}: ${progress}`);
5251
+ }
5252
+ });
5253
+ if (!pulled.ok) {
5254
+ this.showStatus(`Pull failed: ${pulled.error}`);
5255
+ return;
5256
+ }
5257
+ const registration = registerLocalModel({
5258
+ agentDir: getAgentDir(),
5259
+ ref: pullRef,
5260
+ baseUrl: this.localRuntime.baseUrl,
5261
+ });
5262
+ if (!registration.ok) {
5263
+ this.showStatus(`Pulled, but not auto-registered: ${registration.reason}`);
5264
+ if (registration.manualSnippet) {
5265
+ this.showStatus(`Add this to ${registration.modelsJsonPath} yourself:\n${registration.manualSnippet}`);
5266
+ }
5267
+ return;
5268
+ }
5269
+ this.session.modelRegistry.refresh();
5270
+ this.showStatus(`${pullRef} installed and registered as ollama/${pullRef}. Probing fitness…`);
5271
+ await this.runFitnessAndAssign(`ollama/${pullRef}`);
5272
+ }
5273
+ async removeLocalModel(ref, confirmed) {
5274
+ const status = await this.localRuntime.detect();
5275
+ if (!status.serverUp) {
5276
+ this.showStatus("Local server not running — start it (any /models action) before removing.");
5277
+ return;
5278
+ }
5279
+ const models = await this.localRuntime.list();
5280
+ const target = models.find((model) => model.name === ref);
5281
+ if (!target) {
5282
+ this.showStatus(`${ref} is not installed. Installed: ${models.map((model) => model.name).join(", ") || "(none)"}`);
5283
+ return;
5284
+ }
5285
+ if (!confirmed) {
5286
+ // EXPLICIT USER ACTION ONLY: full disclosure, then require the confirm token.
5287
+ const gb = (target.sizeBytes / 1e9).toFixed(2);
5288
+ this.showStatus([
5289
+ `Removing ${ref} will delete:`,
5290
+ ` - model weights (${gb} GB) from ${status.managedByPi ? status.ownedModelsDir : "the system server's storage"}`,
5291
+ ` - the ollama/${ref} entry in models.json`,
5292
+ ` - its cached fitness report for this host`,
5293
+ `Run: /models remove ${ref} confirm`,
5294
+ ].join("\n"));
5295
+ return;
5296
+ }
5297
+ const removed = await this.localRuntime.remove(ref);
5298
+ if (!removed.ok) {
5299
+ this.showStatus(`Remove failed: ${removed.error}`);
5300
+ return;
5301
+ }
5302
+ unregisterLocalModel({ agentDir: getAgentDir(), ref });
5303
+ FitnessStore.forAgentDir(getAgentDir()).remove(`ollama/${ref}`);
5304
+ this.session.modelRegistry.refresh();
5305
+ this.showStatus(`${ref} removed: weights deleted, registration and fitness report dropped.`);
5306
+ }
5132
5307
  /** /fitness with no args: pick a model from the configured registry, probe it, assign a role. */
5133
5308
  showFitnessModelSelector() {
5134
5309
  this.showSelector((done) => {
@@ -5844,6 +6019,18 @@ export class InteractiveMode {
5844
6019
  const loader = this.session.resourceLoader;
5845
6020
  const base = (p) => p.split(/[\\/]/).pop() ?? p;
5846
6021
  const allDiscoverableExtensions = await loader.getDiscoverableExtensionPaths();
6022
+ // Defined BEFORE the skills/prompts arrays below that call it (const = TDZ: defining it
6023
+ // later crashes the whole app with a ReferenceError when the library editor opens).
6024
+ const getFrontmatterDescription = (filePath) => {
6025
+ try {
6026
+ const content = fs.readFileSync(filePath, "utf-8");
6027
+ const { frontmatter } = parseFrontmatter(content);
6028
+ if (typeof frontmatter.description === "string")
6029
+ return frontmatter.description;
6030
+ }
6031
+ catch { }
6032
+ return undefined;
6033
+ };
5847
6034
  // The editor's universe must be profile-INDEPENDENT (discovery, not loading): the loaded
5848
6035
  // getters are narrowed by the active profile, so building the lists from them makes
5849
6036
  // currently-blocked skills/prompts/context files ungrantable — including expanding the
@@ -5895,16 +6082,6 @@ export class InteractiveMode {
5895
6082
  .filter((agentPath) => !loadedAgentPaths.has(agentPath))
5896
6083
  .map((agentPath) => ({ path: agentPath })),
5897
6084
  ];
5898
- const getFrontmatterDescription = (filePath) => {
5899
- try {
5900
- const content = fs.readFileSync(filePath, "utf-8");
5901
- const { frontmatter } = parseFrontmatter(content);
5902
- if (typeof frontmatter.description === "string")
5903
- return frontmatter.description;
5904
- }
5905
- catch { }
5906
- return undefined;
5907
- };
5908
6085
  const getAgentDescription = (filePath) => {
5909
6086
  try {
5910
6087
  const content = fs.readFileSync(filePath, "utf-8");
@@ -6106,6 +6283,8 @@ export class InteractiveMode {
6106
6283
  if (normalizedName.length === 0 || normalizedLower === "none" || normalizedLower === "(none)") {
6107
6284
  try {
6108
6285
  this.settingsManager.setRuntimeResourceProfiles([]);
6286
+ // Clearing must also survive restarts (otherwise the old global selection returns).
6287
+ this.settingsManager.setActiveProfile(undefined, "global");
6109
6288
  this.session.sessionManager.appendCustomEntry("pi.activeResourceProfiles", {
6110
6289
  profiles: [],
6111
6290
  });
@@ -6153,6 +6332,9 @@ export class InteractiveMode {
6153
6332
  this.session.setThinkingLevel(profile.thinking, { persistSettings: false });
6154
6333
  }
6155
6334
  this.settingsManager.setRuntimeResourceProfiles([profile.name]);
6335
+ // Selection must survive pi restarts: persist globally (like model/theme selections).
6336
+ // Runtime + session-entry alone made every /profile choice evaporate on exit.
6337
+ this.settingsManager.setActiveProfile(profile.name, "global");
6156
6338
  this.session.sessionManager.appendCustomEntry("pi.activeResourceProfiles", {
6157
6339
  profiles: [profile.name],
6158
6340
  });