@pasko70/pibo 3.0.2 → 3.1.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.
- package/compute-image/Dockerfile +47 -0
- package/compute-image/Dockerfile.dockerignore +5 -0
- package/dist/agent-runtime/contract.js +78 -28
- package/dist/agent-runtime/portable-history.js +19 -9
- package/dist/agent-runtime/registry.js +8 -2
- package/dist/agent-runtime/routed-session.js +12 -6
- package/dist/agent-runtimes/pi/routed-session.js +10 -4
- package/dist/apps/chat/agent-store.js +55 -3
- package/dist/apps/chat/data/project-service.js +119 -15
- package/dist/apps/chat/data/room-service.js +21 -0
- package/dist/apps/chat/loop-api.js +10 -2
- package/dist/apps/chat/web-app.js +84 -25
- package/dist/apps/chat-ui/assets/{dist-CvaPTBTN.js → dist-CUQJqggN.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-BPotHecb.js → dist-ClUlQWYN.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-BzqQKeVO.js → dist-Djul9BmZ.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-paagejNj.js → dist-GD0JGdCi.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-B6GZgWSj.js → dist-W5HOqHym.js} +1 -1
- package/dist/apps/chat-ui/assets/index-BcjkX-iP.js +228 -0
- package/dist/apps/chat-ui/assets/index-CmqRSbBU.css +1 -0
- package/dist/apps/chat-ui/index.html +2 -2
- package/dist/apps/chat-vscode-web/assets/index-JvrPUGvI.js +43 -0
- package/dist/apps/chat-vscode-web/index.html +1 -1
- package/dist/apps/cli-ui/InkSessionApp.js +111 -12
- package/dist/compute/cli.js +35 -16
- package/dist/compute/docker.js +341 -94
- package/dist/config/config.js +3 -0
- package/dist/core/session-router.js +33 -105
- package/dist/data/pibo-store.js +8 -1
- package/dist/data/schema.js +125 -5
- package/dist/debug/agents.js +91 -89
- package/dist/debug/index.js +2 -0
- package/dist/debug/pty.js +127 -48
- package/dist/gateway/cli.js +4 -0
- package/dist/loops/channel.js +3 -1
- package/dist/loops/cli.js +2 -1
- package/dist/loops/service.js +90 -11
- package/dist/loops/store.js +100 -74
- package/dist/mcp/agent-context.js +13 -26
- package/dist/mcp/commands/info.js +3 -1
- package/dist/mcp/config-command.js +46 -2
- package/dist/mcp/config.js +18 -4
- package/dist/plugins/context-files-store.js +6 -6
- package/dist/previews/base-url.js +34 -0
- package/dist/previews/config.js +2 -32
- package/dist/ralph/store.js +5 -0
- package/dist/reliability/store.js +174 -111
- package/dist/session-ui/terminalRows.js +2 -1
- package/dist/subagents/observation-query.js +121 -0
- package/dist/tools/agent-browser-leases.js +58 -23
- package/dist/tools/agent-browser-wrapper.js +1 -1
- package/dist/tools/browser-use-leases.js +129 -16
- package/dist/tools/browser-use-wrapper.js +1 -1
- package/dist/tools/index.js +27 -11
- package/dist/tools/python-runtime.js +10 -3
- package/dist/tools/registry.js +1 -1
- package/dist/tools/runtime/node-backend.js +16 -5
- package/dist/tools/runtime/node-worker-source.js +112 -33
- package/dist/tools/runtime/python-backend.js +16 -5
- package/dist/tools/runtime/python-worker-source.js +167 -16
- package/dist/tools/runtime/registry.js +23 -10
- package/dist/user-skills/store.js +0 -1
- package/npm-shrinkwrap.json +9 -2
- package/package.json +8 -2
- package/scripts/docker-entrypoint.sh +46 -0
- package/scripts/prepare-agent-browser-wrapper.sh +70 -0
- package/scripts/prepare-browser-use-wrapper.sh +255 -0
- package/dist/apps/chat-ui/assets/index-6JJV-eic.js +0 -228
- package/dist/apps/chat-ui/assets/index-CeL9JPP5.css +0 -1
- package/dist/apps/chat-vscode-web/assets/index-U-MSErGa.js +0 -43
package/dist/loops/store.js
CHANGED
|
@@ -5,6 +5,12 @@ import { DatabaseSync } from 'node:sqlite';
|
|
|
5
5
|
import { piboHomePath } from '../core/pibo-home.js';
|
|
6
6
|
import { isPiboThinkingLevel } from '../core/thinking.js';
|
|
7
7
|
import { addLoopAssistantUsage, newGoalTokenAccounting, normalizeLoopTokenAccounting } from './accounting.js';
|
|
8
|
+
export class PiboLoopActiveRunModeChangeError extends Error {
|
|
9
|
+
constructor() {
|
|
10
|
+
super('Loop mode cannot be changed while a Loop run is active');
|
|
11
|
+
this.name = 'PiboLoopActiveRunModeChangeError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
8
14
|
function nowIso(now = new Date()) { return now.toISOString(); }
|
|
9
15
|
function parseJson(json) { return JSON.parse(json); }
|
|
10
16
|
function defaultName(prompt) { const normalized = prompt.replace(/\s+/g, ' ').trim(); return normalized ? normalized.slice(0, 80) : 'Loop job'; }
|
|
@@ -507,48 +513,56 @@ export class PiboLoopStore {
|
|
|
507
513
|
return this.db.prepare(`SELECT * FROM pibo_ralph_jobs ${where} ORDER BY updated_at DESC, id ASC`).all(...values).map(jobFromRow);
|
|
508
514
|
}
|
|
509
515
|
updateJob(id, patch, now = new Date()) {
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
fastMode: hasOwn(patch, 'fastMode') ? patch.fastMode : existing.fastMode,
|
|
517
|
-
});
|
|
518
|
-
const stopPolicy = hasOwn(patch, 'stopPolicy') ? normalizeLoopStopPolicy(patch.stopPolicy ?? undefined) : existing.stopPolicy;
|
|
519
|
-
const target = patch.target ? normalizeLoopTarget(patch.target) : existing.target;
|
|
520
|
-
const mode = patch.mode !== undefined ? normalizeLoopMode(patch.mode) : existing.mode;
|
|
521
|
-
const enabled = patch.enabled ?? existing.enabled;
|
|
522
|
-
let state = mode === existing.mode
|
|
523
|
-
? { ...existing.state }
|
|
524
|
-
: { completedIterations: existing.state.completedIterations ?? 0, ...(mode === 'goal' ? { goalStatus: enabled ? 'active' : 'paused', tokenAccounting: newGoalTokenAccounting(), tokensUsed: 0, activeTimeSeconds: 0, ...(enabled ? { goalStartedAt: nowIso(now) } : {}) } : {}) };
|
|
525
|
-
if (mode === 'goal' && patch.enabled !== undefined) {
|
|
526
|
-
const currentGoalStatus = goalStatus({ mode, enabled: existing.enabled, state: existing.state }) ?? 'paused';
|
|
527
|
-
if (patch.enabled) {
|
|
528
|
-
if (currentGoalStatus === 'complete')
|
|
529
|
-
throw new Error('Completed goals cannot be restarted; create a new goal');
|
|
530
|
-
const nextBudget = hasOwn(patch, 'tokenBudget') ? normalizeTokenBudget(patch.tokenBudget ?? undefined) : existing.tokenBudget;
|
|
531
|
-
const nextReserve = hasOwn(patch, 'tokenReserve') ? normalizeTokenReserve(patch.tokenReserve ?? undefined) : existing.tokenReserve;
|
|
532
|
-
if (currentGoalStatus === 'budget_limited' && nextBudget !== undefined && (existing.state.tokensUsed ?? 0) + (nextReserve ?? 0) >= nextBudget)
|
|
533
|
-
throw new Error('Increase or clear the token budget, or lower the token reserve, before resuming this goal');
|
|
534
|
-
state.goalStatus = 'active';
|
|
535
|
-
state.goalStartedAt ??= nowIso(now);
|
|
536
|
-
delete state.goalEndedAt;
|
|
537
|
-
state.stopRequestedAt = undefined;
|
|
538
|
-
state.cancelRequestedAt = undefined;
|
|
539
|
-
state.lastFailure = undefined;
|
|
540
|
-
state.nextAttemptAt = undefined;
|
|
541
|
-
state.retryBackoffMs = undefined;
|
|
542
|
-
state.consecutiveErrors = 0;
|
|
516
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
517
|
+
try {
|
|
518
|
+
const existing = this.getJob(id);
|
|
519
|
+
if (!existing) {
|
|
520
|
+
this.db.exec('COMMIT');
|
|
521
|
+
return undefined;
|
|
543
522
|
}
|
|
544
|
-
|
|
545
|
-
|
|
523
|
+
const runtimeOptions = normalizeRuntimeOptions({
|
|
524
|
+
modelOverride: hasOwn(patch, 'modelOverride') ? patch.modelOverride : existing.modelOverride,
|
|
525
|
+
thinkingLevel: hasOwn(patch, 'thinkingLevel') ? patch.thinkingLevel : existing.thinkingLevel,
|
|
526
|
+
fastMode: hasOwn(patch, 'fastMode') ? patch.fastMode : existing.fastMode,
|
|
527
|
+
});
|
|
528
|
+
const stopPolicy = hasOwn(patch, 'stopPolicy') ? normalizeLoopStopPolicy(patch.stopPolicy ?? undefined) : existing.stopPolicy;
|
|
529
|
+
const target = patch.target ? normalizeLoopTarget(patch.target) : existing.target;
|
|
530
|
+
const mode = patch.mode !== undefined ? normalizeLoopMode(patch.mode) : existing.mode;
|
|
531
|
+
if (mode !== existing.mode && this.db.prepare("SELECT 1 FROM pibo_ralph_runs WHERE job_id = ? AND status = 'running' LIMIT 1").get(id))
|
|
532
|
+
throw new PiboLoopActiveRunModeChangeError();
|
|
533
|
+
const enabled = patch.enabled ?? existing.enabled;
|
|
534
|
+
let state = mode === existing.mode
|
|
535
|
+
? { ...existing.state }
|
|
536
|
+
: { completedIterations: existing.state.completedIterations ?? 0, ...(mode === 'goal' ? { goalStatus: enabled ? 'active' : 'paused', tokenAccounting: newGoalTokenAccounting(), tokensUsed: 0, activeTimeSeconds: 0, ...(enabled ? { goalStartedAt: nowIso(now) } : {}) } : {}) };
|
|
537
|
+
if (mode === 'goal' && patch.enabled !== undefined) {
|
|
538
|
+
const currentGoalStatus = goalStatus({ mode, enabled: existing.enabled, state: existing.state }) ?? 'paused';
|
|
539
|
+
if (patch.enabled) {
|
|
540
|
+
if (isTerminalGoalStatus(currentGoalStatus))
|
|
541
|
+
throw new Error('Terminal Goals cannot be restarted; use the confirmed Goal reopen operation');
|
|
542
|
+
state.goalStatus = 'active';
|
|
543
|
+
state.goalStartedAt ??= nowIso(now);
|
|
544
|
+
delete state.goalEndedAt;
|
|
545
|
+
state.stopRequestedAt = undefined;
|
|
546
|
+
state.cancelRequestedAt = undefined;
|
|
547
|
+
state.lastFailure = undefined;
|
|
548
|
+
state.nextAttemptAt = undefined;
|
|
549
|
+
state.retryBackoffMs = undefined;
|
|
550
|
+
state.consecutiveErrors = 0;
|
|
551
|
+
}
|
|
552
|
+
else {
|
|
553
|
+
state.goalStatus = currentGoalStatus === 'active' ? 'paused' : currentGoalStatus;
|
|
554
|
+
}
|
|
546
555
|
}
|
|
556
|
+
const next = { ...existing, mode, state, name: patch.name !== undefined ? patch.name.trim() : existing.name, description: patch.description !== undefined ? patch.description?.trim() || undefined : existing.description, enabled, target, profile: patch.profile ?? existing.profile, prompt: patch.prompt ?? existing.prompt, maxIterations: hasOwn(patch, 'maxIterations') ? normalizeMaxIterations(patch.maxIterations ?? undefined) : existing.maxIterations, tokenBudget: mode === 'ralph' ? undefined : hasOwn(patch, 'tokenBudget') ? normalizeTokenBudget(patch.tokenBudget ?? undefined) : existing.tokenBudget, tokenReserve: mode === 'ralph' || (hasOwn(patch, 'tokenBudget') && patch.tokenBudget === null && !hasOwn(patch, 'tokenReserve')) ? undefined : hasOwn(patch, 'tokenReserve') ? normalizeTokenReserve(patch.tokenReserve ?? undefined) : existing.tokenReserve, stopPolicy, modelOverride: runtimeOptions.modelOverride, thinkingLevel: runtimeOptions.thinkingLevel, fastMode: runtimeOptions.fastMode, updatedAt: nowIso(now) };
|
|
557
|
+
validateJobInput(next);
|
|
558
|
+
this.writeJob(next);
|
|
559
|
+
this.db.exec('COMMIT');
|
|
560
|
+
return this.getJob(id);
|
|
561
|
+
}
|
|
562
|
+
catch (error) {
|
|
563
|
+
this.db.exec('ROLLBACK');
|
|
564
|
+
throw error;
|
|
547
565
|
}
|
|
548
|
-
const next = { ...existing, mode, state, name: patch.name !== undefined ? patch.name.trim() : existing.name, description: patch.description !== undefined ? patch.description?.trim() || undefined : existing.description, enabled, target, profile: patch.profile ?? existing.profile, prompt: patch.prompt ?? existing.prompt, maxIterations: hasOwn(patch, 'maxIterations') ? normalizeMaxIterations(patch.maxIterations ?? undefined) : existing.maxIterations, tokenBudget: mode === 'ralph' ? undefined : hasOwn(patch, 'tokenBudget') ? normalizeTokenBudget(patch.tokenBudget ?? undefined) : existing.tokenBudget, tokenReserve: mode === 'ralph' || (hasOwn(patch, 'tokenBudget') && patch.tokenBudget === null && !hasOwn(patch, 'tokenReserve')) ? undefined : hasOwn(patch, 'tokenReserve') ? normalizeTokenReserve(patch.tokenReserve ?? undefined) : existing.tokenReserve, stopPolicy, modelOverride: runtimeOptions.modelOverride, thinkingLevel: runtimeOptions.thinkingLevel, fastMode: runtimeOptions.fastMode, updatedAt: nowIso(now) };
|
|
549
|
-
validateJobInput(next);
|
|
550
|
-
this.writeJob(next);
|
|
551
|
-
return this.getJob(id);
|
|
552
566
|
}
|
|
553
567
|
updateJobResources(id, resources, now = new Date()) {
|
|
554
568
|
const existing = this.getJob(id);
|
|
@@ -621,6 +635,7 @@ export class PiboLoopStore {
|
|
|
621
635
|
return Number(result.changes ?? 0) > 0 ? this.getRunByMessageEventId(eventId) : undefined;
|
|
622
636
|
}
|
|
623
637
|
reserveRun(id, now = new Date()) { this.updateJob(id, { enabled: true }, now); return this.reserveJob(id, now); }
|
|
638
|
+
reserveAdmittedRun(id, now = new Date()) { return this.reserveJob(id, now, true); }
|
|
624
639
|
reserveDueRuns(limit, now = new Date()) {
|
|
625
640
|
const rows = this.db.prepare('SELECT * FROM pibo_ralph_jobs WHERE enabled = 1 ORDER BY updated_at ASC').all();
|
|
626
641
|
const result = [];
|
|
@@ -667,41 +682,47 @@ export class PiboLoopStore {
|
|
|
667
682
|
}
|
|
668
683
|
completeRun(input, now = new Date()) {
|
|
669
684
|
const timestamp = nowIso(now);
|
|
670
|
-
const job = this.getJob(input.jobId);
|
|
671
|
-
if (!job)
|
|
672
|
-
return;
|
|
673
|
-
const completedIterations = (job.state.completedIterations ?? 0) + 1;
|
|
674
|
-
const reachedMaxIterations = job.maxIterations !== undefined && completedIterations >= job.maxIterations;
|
|
675
|
-
const currentGoalStatus = goalStatus(job);
|
|
676
|
-
const nextGoalStatus = job.mode === 'goal'
|
|
677
|
-
? isTerminalGoalStatus(currentGoalStatus) || currentGoalStatus === 'paused'
|
|
678
|
-
? currentGoalStatus
|
|
679
|
-
: input.goalStatus ?? currentGoalStatus
|
|
680
|
-
: undefined;
|
|
681
|
-
const terminalGoalStatus = job.mode === 'goal' && isTerminalGoalStatus(nextGoalStatus);
|
|
682
|
-
const shouldDisable = terminalGoalStatus || reachedMaxIterations || input.stopAfterRun === true || input.stopEvaluation?.finalAction === 'stop-after-run' || input.stopEvaluation?.finalAction === 'cancel-current-run';
|
|
683
|
-
const state = {
|
|
684
|
-
...job.state,
|
|
685
|
-
...(job.mode === 'goal' ? { activeTimeRunningAt: null } : {}),
|
|
686
|
-
runningAt: undefined,
|
|
687
|
-
completedIterations,
|
|
688
|
-
lastRunAt: timestamp,
|
|
689
|
-
lastRunId: input.runId,
|
|
690
|
-
lastStatus: input.status === 'error' ? 'error' : input.status === 'cancelled' ? 'cancelled' : 'ok',
|
|
691
|
-
lastError: input.error,
|
|
692
|
-
lastFailure: input.status === 'error' ? input.failure : undefined,
|
|
693
|
-
nextAttemptAt: input.status === 'error' ? input.failure?.nextAttemptAt : undefined,
|
|
694
|
-
retryBackoffMs: input.status === 'error' ? input.failure?.retryBackoffMs : undefined,
|
|
695
|
-
lastPiboSessionId: input.piboSessionId ?? job.state.lastPiboSessionId,
|
|
696
|
-
consecutiveErrors: input.status === 'error' ? (job.state.consecutiveErrors ?? 0) + 1 : 0,
|
|
697
|
-
conditionStates: input.conditionStates ?? job.state.conditionStates,
|
|
698
|
-
lastStopEvaluation: input.stopEvaluation ?? job.state.lastStopEvaluation,
|
|
699
|
-
...(nextGoalStatus ? { goalStatus: nextGoalStatus } : {}),
|
|
700
|
-
...(terminalGoalStatus ? { goalEndedAt: job.state.goalEndedAt ?? timestamp } : {}),
|
|
701
|
-
};
|
|
702
685
|
this.db.exec('BEGIN IMMEDIATE');
|
|
703
686
|
try {
|
|
704
|
-
this.db.prepare("UPDATE pibo_ralph_runs SET status = ?, pibo_session_id = COALESCE(?, pibo_session_id), reason = ?, error = ?, error_details_json = ?, message_state = CASE WHEN message_state = 'invalidated' THEN message_state ELSE 'finished' END, completed_at = ?, updated_at = ? WHERE id = ?").run(input.status, input.piboSessionId ?? null, input.reason ?? input.stopEvaluation?.reason ?? null, input.error ?? null, sessionErrorDetailsJson(input.errorDetails), timestamp, timestamp, input.runId);
|
|
687
|
+
const result = this.db.prepare("UPDATE pibo_ralph_runs SET status = ?, pibo_session_id = COALESCE(?, pibo_session_id), reason = ?, error = ?, error_details_json = ?, message_state = CASE WHEN message_state = 'invalidated' THEN message_state ELSE 'finished' END, completed_at = ?, updated_at = ? WHERE id = ? AND job_id = ? AND status = 'running'").run(input.status, input.piboSessionId ?? null, input.reason ?? input.stopEvaluation?.reason ?? null, input.error ?? null, sessionErrorDetailsJson(input.errorDetails), timestamp, timestamp, input.runId, input.jobId);
|
|
688
|
+
if (Number(result.changes ?? 0) === 0) {
|
|
689
|
+
this.db.exec('COMMIT');
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
const job = this.getJob(input.jobId);
|
|
693
|
+
if (!job || !job.state.runningAt || job.state.lastRunId !== input.runId) {
|
|
694
|
+
this.db.exec('COMMIT');
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
const completedIterations = (job.state.completedIterations ?? 0) + 1;
|
|
698
|
+
const reachedMaxIterations = job.maxIterations !== undefined && completedIterations >= job.maxIterations;
|
|
699
|
+
const currentGoalStatus = goalStatus(job);
|
|
700
|
+
const nextGoalStatus = job.mode === 'goal'
|
|
701
|
+
? isTerminalGoalStatus(currentGoalStatus) || currentGoalStatus === 'paused'
|
|
702
|
+
? currentGoalStatus
|
|
703
|
+
: input.goalStatus ?? currentGoalStatus
|
|
704
|
+
: undefined;
|
|
705
|
+
const terminalGoalStatus = job.mode === 'goal' && isTerminalGoalStatus(nextGoalStatus);
|
|
706
|
+
const shouldDisable = terminalGoalStatus || reachedMaxIterations || input.stopAfterRun === true || input.stopEvaluation?.finalAction === 'stop-after-run' || input.stopEvaluation?.finalAction === 'cancel-current-run';
|
|
707
|
+
const state = {
|
|
708
|
+
...job.state,
|
|
709
|
+
...(job.mode === 'goal' ? { activeTimeRunningAt: null } : {}),
|
|
710
|
+
runningAt: undefined,
|
|
711
|
+
completedIterations,
|
|
712
|
+
lastRunAt: timestamp,
|
|
713
|
+
lastRunId: input.runId,
|
|
714
|
+
lastStatus: input.status === 'error' ? 'error' : input.status === 'cancelled' ? 'cancelled' : 'ok',
|
|
715
|
+
lastError: input.error,
|
|
716
|
+
lastFailure: input.status === 'error' ? input.failure : undefined,
|
|
717
|
+
nextAttemptAt: input.status === 'error' ? input.failure?.nextAttemptAt : undefined,
|
|
718
|
+
retryBackoffMs: input.status === 'error' ? input.failure?.retryBackoffMs : undefined,
|
|
719
|
+
lastPiboSessionId: input.piboSessionId ?? job.state.lastPiboSessionId,
|
|
720
|
+
consecutiveErrors: input.status === 'error' ? (job.state.consecutiveErrors ?? 0) + 1 : 0,
|
|
721
|
+
conditionStates: input.conditionStates ?? job.state.conditionStates,
|
|
722
|
+
lastStopEvaluation: input.stopEvaluation ?? job.state.lastStopEvaluation,
|
|
723
|
+
...(nextGoalStatus ? { goalStatus: nextGoalStatus } : {}),
|
|
724
|
+
...(terminalGoalStatus ? { goalEndedAt: job.state.goalEndedAt ?? timestamp } : {}),
|
|
725
|
+
};
|
|
705
726
|
this.db.prepare('UPDATE pibo_ralph_jobs SET enabled = ?, state_json = ?, updated_at = ? WHERE id = ?').run(shouldDisable ? 0 : job.enabled ? 1 : 0, JSON.stringify(state), timestamp, job.id);
|
|
706
727
|
this.db.exec('COMMIT');
|
|
707
728
|
}
|
|
@@ -768,12 +789,17 @@ export class PiboLoopStore {
|
|
|
768
789
|
this.updateRunResources({ jobId: job.id, runId, resources: nextResources });
|
|
769
790
|
this.updateJobResources(job.id, nextResources);
|
|
770
791
|
}
|
|
771
|
-
reserveJob(id, now = new Date()) {
|
|
792
|
+
reserveJob(id, now = new Date(), requireAdmission = false) {
|
|
772
793
|
const timestamp = nowIso(now);
|
|
773
794
|
this.db.exec('BEGIN IMMEDIATE');
|
|
774
795
|
try {
|
|
775
796
|
const job = this.getJob(id);
|
|
776
|
-
if (!job || !job.enabled || job.state.runningAt) {
|
|
797
|
+
if (!job || !job.enabled || job.state.runningAt || (requireAdmission && job.state.cancelRequestedAt !== undefined)) {
|
|
798
|
+
this.db.exec('COMMIT');
|
|
799
|
+
return undefined;
|
|
800
|
+
}
|
|
801
|
+
if (job.maxIterations !== undefined && (job.state.completedIterations ?? 0) >= job.maxIterations) {
|
|
802
|
+
this.db.prepare('UPDATE pibo_ralph_jobs SET enabled = 0, updated_at = ? WHERE id = ?').run(timestamp, job.id);
|
|
777
803
|
this.db.exec('COMMIT');
|
|
778
804
|
return undefined;
|
|
779
805
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { stat, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { isHttpServer, loadConfigUnresolved, resolveMcpServerConfigSource, } from './config.js';
|
|
3
3
|
import { ErrorCode, formatCliError } from './errors.js';
|
|
4
4
|
export const MCP_SERVER_DESCRIPTION_MAX_LENGTH = 480;
|
|
5
5
|
export const ENABLED_MCP_SERVERS_CONTEXT_PATH = '.pibo/context/enabled-mcp-servers.md';
|
|
@@ -34,15 +34,19 @@ export async function listMcpServerInfos(configPath) {
|
|
|
34
34
|
}
|
|
35
35
|
export async function setMcpServerDescription(serverName, descriptionInput, configPath) {
|
|
36
36
|
const description = normalizeMcpServerDescription(descriptionInput);
|
|
37
|
-
const path = await
|
|
38
|
-
|
|
39
|
-
const server = config.mcpServers[serverName];
|
|
40
|
-
if (!server) {
|
|
37
|
+
const { path, config, server } = await resolveMcpServerConfigSource(serverName, configPath);
|
|
38
|
+
if (server.pibo?.descriptionSource === 'registry') {
|
|
41
39
|
throw new Error(formatCliError({
|
|
42
40
|
code: ErrorCode.CLIENT_ERROR,
|
|
43
|
-
type: '
|
|
44
|
-
message: `Server "${serverName}"
|
|
45
|
-
|
|
41
|
+
type: 'MCP_DESCRIPTION_READ_ONLY',
|
|
42
|
+
message: `Server "${serverName}" description is read-only`,
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
if (process.platform !== 'win32' && ((await stat(path)).mode & 0o222) === 0) {
|
|
46
|
+
throw new Error(formatCliError({
|
|
47
|
+
code: ErrorCode.CLIENT_ERROR,
|
|
48
|
+
type: 'MCP_CONFIG_READ_ONLY',
|
|
49
|
+
message: `Server "${serverName}" config is read-only`,
|
|
46
50
|
}));
|
|
47
51
|
}
|
|
48
52
|
server.pibo = {
|
|
@@ -91,23 +95,6 @@ export function getMcpAgentContextFileFromConfig(selectedServerNames, config) {
|
|
|
91
95
|
].join('\n').trimEnd(),
|
|
92
96
|
};
|
|
93
97
|
}
|
|
94
|
-
async function readMcpConfig(path) {
|
|
95
|
-
const content = await readFile(path, 'utf-8');
|
|
96
|
-
const parsed = JSON.parse(content);
|
|
97
|
-
if (!parsed ||
|
|
98
|
-
typeof parsed !== 'object' ||
|
|
99
|
-
!('mcpServers' in parsed) ||
|
|
100
|
-
typeof parsed.mcpServers !== 'object' ||
|
|
101
|
-
parsed.mcpServers === null) {
|
|
102
|
-
throw new Error(formatCliError({
|
|
103
|
-
code: ErrorCode.CLIENT_ERROR,
|
|
104
|
-
type: 'CONFIG_MISSING_FIELD',
|
|
105
|
-
message: 'Config file missing required "mcpServers" object',
|
|
106
|
-
details: `File: ${path}`,
|
|
107
|
-
}));
|
|
108
|
-
}
|
|
109
|
-
return parsed;
|
|
110
|
-
}
|
|
111
98
|
function mcpServerInfoFromConfig(name, server) {
|
|
112
99
|
const metadata = normalizeMetadata(server.pibo);
|
|
113
100
|
const description = metadata.description;
|
|
@@ -28,7 +28,9 @@ export async function infoCommand(options) {
|
|
|
28
28
|
process.exit(ErrorCode.CLIENT_ERROR);
|
|
29
29
|
}
|
|
30
30
|
const { server: serverName, tool: toolName } = parseTarget(options.target);
|
|
31
|
-
const serverConfig = config.mcpServers
|
|
31
|
+
const serverConfig = Object.hasOwn(config.mcpServers, serverName)
|
|
32
|
+
? config.mcpServers[serverName]
|
|
33
|
+
: undefined;
|
|
32
34
|
if (!serverConfig) {
|
|
33
35
|
const available = Object.keys(config.mcpServers);
|
|
34
36
|
const serverList = available.length > 0 ? available.join(', ') : '(none)';
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
3
|
-
import {
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { CustomAgentStore } from '../apps/chat/agent-store.js';
|
|
5
|
+
import { piboHomePath } from '../core/pibo-home.js';
|
|
6
|
+
import { ensureConfigExists, findConfigPath, getConfigSearchPaths, getPreferredConfigPath, } from './config.js';
|
|
4
7
|
import { ErrorCode, formatCliError } from './errors.js';
|
|
5
8
|
import { setMcpServerDescription } from './agent-context.js';
|
|
6
9
|
const EXAMPLE_CONFIG = {
|
|
@@ -97,6 +100,35 @@ async function readRawConfig(path) {
|
|
|
97
100
|
async function writeRawConfig(path, config) {
|
|
98
101
|
await writeFile(path, `${JSON.stringify(config, null, 2)}\n`);
|
|
99
102
|
}
|
|
103
|
+
async function mergedConfigRetainsServer(name, updatedPath, updatedConfig, explicitPath) {
|
|
104
|
+
const resolvedUpdatedPath = resolve(updatedPath);
|
|
105
|
+
for (const sourcePath of getConfigSearchPaths(explicitPath)) {
|
|
106
|
+
if (!existsSync(sourcePath))
|
|
107
|
+
continue;
|
|
108
|
+
const sourceConfig = resolve(sourcePath) === resolvedUpdatedPath
|
|
109
|
+
? updatedConfig
|
|
110
|
+
: await readRawConfig(sourcePath);
|
|
111
|
+
if (Object.hasOwn(sourceConfig.mcpServers, name))
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
function customAgentsSelectingMcpServer(name) {
|
|
117
|
+
const storePath = piboHomePath('chat-agents.sqlite');
|
|
118
|
+
if (!existsSync(storePath))
|
|
119
|
+
return [];
|
|
120
|
+
const store = new CustomAgentStore(storePath);
|
|
121
|
+
try {
|
|
122
|
+
return store
|
|
123
|
+
.list({ includeArchived: true })
|
|
124
|
+
.filter((agent) => agent.mcpServers.includes(name))
|
|
125
|
+
.map((agent) => agent.profileName)
|
|
126
|
+
.sort((left, right) => left.localeCompare(right));
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
store.close();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
100
132
|
function parseServerConfig(input) {
|
|
101
133
|
let parsed;
|
|
102
134
|
try {
|
|
@@ -205,7 +237,7 @@ export async function configCommand(options) {
|
|
|
205
237
|
suggestion: 'Example: pibo mcp config remove filesystem',
|
|
206
238
|
}));
|
|
207
239
|
}
|
|
208
|
-
if (!(options.name
|
|
240
|
+
if (!Object.hasOwn(config.mcpServers, options.name)) {
|
|
209
241
|
throw new Error(formatCliError({
|
|
210
242
|
code: ErrorCode.CLIENT_ERROR,
|
|
211
243
|
type: 'SERVER_NOT_FOUND',
|
|
@@ -214,6 +246,18 @@ export async function configCommand(options) {
|
|
|
214
246
|
}));
|
|
215
247
|
}
|
|
216
248
|
delete config.mcpServers[options.name];
|
|
249
|
+
if (!(await mergedConfigRetainsServer(options.name, path, config, options.configPath))) {
|
|
250
|
+
const affectedAgents = customAgentsSelectingMcpServer(options.name);
|
|
251
|
+
if (affectedAgents.length > 0) {
|
|
252
|
+
throw new Error(formatCliError({
|
|
253
|
+
code: ErrorCode.CLIENT_ERROR,
|
|
254
|
+
type: 'MCP_SERVER_IN_USE',
|
|
255
|
+
message: `MCP server "${options.name}" is selected by custom agents`,
|
|
256
|
+
details: `Custom agents: ${affectedAgents.join(', ')}`,
|
|
257
|
+
suggestion: 'Update those agents to stop selecting this MCP server, then retry the removal.',
|
|
258
|
+
}));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
217
261
|
await writeRawConfig(path, config);
|
|
218
262
|
console.log(`Removed MCP server "${options.name}" from ${path}`);
|
|
219
263
|
}
|
package/dist/mcp/config.js
CHANGED
|
@@ -496,7 +496,7 @@ export async function loadConfigUnresolved(explicitPath) {
|
|
|
496
496
|
for (const configPath of existingPaths) {
|
|
497
497
|
const config = await readRawConfig(configPath);
|
|
498
498
|
for (const [serverName, serverConfig] of Object.entries(config.mcpServers)) {
|
|
499
|
-
if (!(
|
|
499
|
+
if (!Object.hasOwn(merged.mcpServers, serverName)) {
|
|
500
500
|
merged.mcpServers[serverName] = serverConfig;
|
|
501
501
|
}
|
|
502
502
|
}
|
|
@@ -506,6 +506,21 @@ export async function loadConfigUnresolved(explicitPath) {
|
|
|
506
506
|
}
|
|
507
507
|
return merged;
|
|
508
508
|
}
|
|
509
|
+
/**
|
|
510
|
+
* Resolve the highest-priority config file that contributes a merged server.
|
|
511
|
+
*/
|
|
512
|
+
export async function resolveMcpServerConfigSource(serverName, explicitPath) {
|
|
513
|
+
const merged = await loadConfigUnresolved(explicitPath);
|
|
514
|
+
for (const configPath of getConfigSearchPaths(explicitPath)) {
|
|
515
|
+
if (!existsSync(configPath))
|
|
516
|
+
continue;
|
|
517
|
+
const config = await readRawConfig(configPath);
|
|
518
|
+
if (Object.hasOwn(config.mcpServers, serverName)) {
|
|
519
|
+
return { path: configPath, config, server: config.mcpServers[serverName] };
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
throw new Error(formatCliError(serverNotFoundError(serverName, Object.keys(merged.mcpServers))));
|
|
523
|
+
}
|
|
509
524
|
export async function loadConfig(explicitPath) {
|
|
510
525
|
return substituteEnvVarsInObject(await loadConfigUnresolved(explicitPath));
|
|
511
526
|
}
|
|
@@ -539,12 +554,11 @@ export function formatConfigSourceSummaries(summaries) {
|
|
|
539
554
|
* Get a specific server config by name
|
|
540
555
|
*/
|
|
541
556
|
export function getServerConfig(config, serverName) {
|
|
542
|
-
|
|
543
|
-
if (!server) {
|
|
557
|
+
if (!Object.hasOwn(config.mcpServers, serverName)) {
|
|
544
558
|
const available = Object.keys(config.mcpServers);
|
|
545
559
|
throw new Error(formatCliError(serverNotFoundError(serverName, available)));
|
|
546
560
|
}
|
|
547
|
-
return
|
|
561
|
+
return config.mcpServers[serverName];
|
|
548
562
|
}
|
|
549
563
|
/**
|
|
550
564
|
* List all server names
|
|
@@ -445,6 +445,12 @@ export class ContextFileMetadataStore {
|
|
|
445
445
|
WHERE key = ?
|
|
446
446
|
`).run(file.workingContent, file.sourceContent ?? null, file.key);
|
|
447
447
|
}
|
|
448
|
+
for (const file of recovered) {
|
|
449
|
+
if (!file.restoreManagedFile)
|
|
450
|
+
continue;
|
|
451
|
+
mkdirSync(dirname(file.managedPath), { recursive: true });
|
|
452
|
+
writeFileSync(file.managedPath, file.workingContent, "utf8");
|
|
453
|
+
}
|
|
448
454
|
const migratedAt = new Date().toISOString();
|
|
449
455
|
this.db.prepare(`
|
|
450
456
|
INSERT INTO context_file_store_meta (key, value) VALUES (?, ?)
|
|
@@ -460,12 +466,6 @@ export class ContextFileMetadataStore {
|
|
|
460
466
|
this.db.exec("ROLLBACK");
|
|
461
467
|
throw error;
|
|
462
468
|
}
|
|
463
|
-
for (const file of recovered) {
|
|
464
|
-
if (!file.restoreManagedFile)
|
|
465
|
-
continue;
|
|
466
|
-
mkdirSync(dirname(file.managedPath), { recursive: true });
|
|
467
|
-
writeFileSync(file.managedPath, file.workingContent, "utf8");
|
|
468
|
-
}
|
|
469
469
|
}
|
|
470
470
|
migrateLegacyStore() {
|
|
471
471
|
if (!this.legacyStorePath)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { isIP } from "node:net";
|
|
2
|
+
function isLocalPreviewHostname(hostname) {
|
|
3
|
+
return hostname === "localhost" || hostname.endsWith(".localhost");
|
|
4
|
+
}
|
|
5
|
+
function isValidDnsHostname(hostname) {
|
|
6
|
+
return isIP(hostname) === 0 &&
|
|
7
|
+
hostname.length <= 253 &&
|
|
8
|
+
!hostname.endsWith(".") &&
|
|
9
|
+
hostname.split(".").every((label) => label.length >= 1 &&
|
|
10
|
+
label.length <= 63 &&
|
|
11
|
+
/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i.test(label));
|
|
12
|
+
}
|
|
13
|
+
export function parsePreviewBaseURL(value) {
|
|
14
|
+
let url;
|
|
15
|
+
try {
|
|
16
|
+
url = new URL(value);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
throw new Error("preview.baseURL must be an absolute HTTP or HTTPS URL");
|
|
20
|
+
}
|
|
21
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
22
|
+
throw new Error("preview.baseURL must use http or https");
|
|
23
|
+
}
|
|
24
|
+
if (url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
|
|
25
|
+
throw new Error("preview.baseURL must contain only scheme, hostname, and optional port");
|
|
26
|
+
}
|
|
27
|
+
if (!isValidDnsHostname(url.hostname)) {
|
|
28
|
+
throw new Error("preview.baseURL hostname must be a DNS hostname without wildcards, IP literals, or a trailing dot");
|
|
29
|
+
}
|
|
30
|
+
if (url.protocol === "http:" && !isLocalPreviewHostname(url.hostname)) {
|
|
31
|
+
throw new Error("preview.baseURL must use https except for localhost development");
|
|
32
|
+
}
|
|
33
|
+
return url;
|
|
34
|
+
}
|
package/dist/previews/config.js
CHANGED
|
@@ -1,45 +1,15 @@
|
|
|
1
|
-
import { isIP } from "node:net";
|
|
2
1
|
import { loadPiboConfig } from "../config/config.js";
|
|
2
|
+
import { parsePreviewBaseURL } from "./base-url.js";
|
|
3
3
|
export const DEFAULT_PREVIEW_TTL_MINUTES = 8 * 60;
|
|
4
4
|
export const DEFAULT_PREVIEW_TICKET_TTL_SECONDS = 60;
|
|
5
5
|
export const DEFAULT_PREVIEW_SESSION_TTL_MINUTES = 8 * 60;
|
|
6
|
-
function isLocalPreviewHostname(hostname) {
|
|
7
|
-
return hostname === "localhost" || hostname.endsWith(".localhost");
|
|
8
|
-
}
|
|
9
|
-
function isValidDnsHostname(hostname) {
|
|
10
|
-
return isIP(hostname) === 0 &&
|
|
11
|
-
hostname.length <= 253 &&
|
|
12
|
-
!hostname.endsWith(".") &&
|
|
13
|
-
hostname.split(".").every((label) => label.length >= 1 &&
|
|
14
|
-
label.length <= 63 &&
|
|
15
|
-
/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i.test(label));
|
|
16
|
-
}
|
|
17
6
|
export function loadPreviewConfig() {
|
|
18
7
|
return loadPiboConfig().preview ?? {};
|
|
19
8
|
}
|
|
20
9
|
export function requirePreviewBaseURL(value = loadPreviewConfig().baseURL) {
|
|
21
10
|
if (!value)
|
|
22
11
|
throw new Error("preview.baseURL is required. Set it with `pibo config set preview.baseURL https://preview.example.com`.");
|
|
23
|
-
|
|
24
|
-
try {
|
|
25
|
-
url = new URL(value);
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
throw new Error("preview.baseURL must be an absolute HTTP or HTTPS URL");
|
|
29
|
-
}
|
|
30
|
-
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
31
|
-
throw new Error("preview.baseURL must use http or https");
|
|
32
|
-
}
|
|
33
|
-
if (url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
|
|
34
|
-
throw new Error("preview.baseURL must contain only scheme, hostname, and optional port");
|
|
35
|
-
}
|
|
36
|
-
if (!isValidDnsHostname(url.hostname)) {
|
|
37
|
-
throw new Error("preview.baseURL hostname must be a DNS hostname without wildcards, IP literals, or a trailing dot");
|
|
38
|
-
}
|
|
39
|
-
if (url.protocol === "http:" && !isLocalPreviewHostname(url.hostname)) {
|
|
40
|
-
throw new Error("preview.baseURL must use https except for localhost development");
|
|
41
|
-
}
|
|
42
|
-
return url;
|
|
12
|
+
return parsePreviewBaseURL(value);
|
|
43
13
|
}
|
|
44
14
|
export function previewPublicURL(previewId, baseURL = requirePreviewBaseURL()) {
|
|
45
15
|
if (previewId.length > 63 || !/^pv-[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(previewId)) {
|
package/dist/ralph/store.js
CHANGED
|
@@ -407,6 +407,11 @@ export class PiboRalphStore {
|
|
|
407
407
|
this.db.exec('COMMIT');
|
|
408
408
|
return undefined;
|
|
409
409
|
}
|
|
410
|
+
if (job.maxIterations !== undefined && (job.state.completedIterations ?? 0) >= job.maxIterations) {
|
|
411
|
+
this.db.prepare('UPDATE pibo_ralph_jobs SET enabled = 0, updated_at = ? WHERE id = ?').run(timestamp, job.id);
|
|
412
|
+
this.db.exec('COMMIT');
|
|
413
|
+
return undefined;
|
|
414
|
+
}
|
|
410
415
|
const run = this.createRunLocked(job, timestamp);
|
|
411
416
|
const state = { ...job.state, runningAt: timestamp, lastRunAt: timestamp, lastRunId: run.id };
|
|
412
417
|
this.updateJobStateLocked(job.id, state, timestamp);
|