@getpaseo/server 0.1.92 → 0.1.94

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/dist/server/server/agent/agent-manager.d.ts +14 -0
  2. package/dist/server/server/agent/agent-manager.js +36 -0
  3. package/dist/server/server/agent/agent-sdk-types.d.ts +1 -0
  4. package/dist/server/server/agent/providers/claude/agent.d.ts +1 -0
  5. package/dist/server/server/agent/providers/claude/agent.js +58 -3
  6. package/dist/server/server/agent/providers/claude/models.js +15 -0
  7. package/dist/server/server/agent/providers/claude/task-notification-tool-call.d.ts +2 -2
  8. package/dist/server/server/agent/providers/mock-load-test-agent.js +55 -28
  9. package/dist/server/server/agent/providers/pi/agent.js +3 -1
  10. package/dist/server/server/agent/providers/pi/session-descriptor.d.ts +5 -0
  11. package/dist/server/server/agent/providers/pi/session-descriptor.js +74 -12
  12. package/dist/server/server/agent/runtime-mcp-config.d.ts +6 -0
  13. package/dist/server/server/agent/runtime-mcp-config.js +3 -0
  14. package/dist/server/server/auth.d.ts +13 -0
  15. package/dist/server/server/auth.js +35 -0
  16. package/dist/server/server/bootstrap.d.ts +2 -0
  17. package/dist/server/server/bootstrap.js +28 -1
  18. package/dist/server/server/config.js +3 -1
  19. package/dist/server/server/daemon-config-store.js +3 -0
  20. package/dist/server/server/loop-service.d.ts +6 -6
  21. package/dist/server/server/persisted-config.d.ts +61 -0
  22. package/dist/server/server/persisted-config.js +2 -0
  23. package/dist/server/server/session.d.ts +1 -0
  24. package/dist/server/server/session.js +50 -3
  25. package/dist/server/server/websocket-server.js +2 -0
  26. package/dist/server/server/workspace-git-service.d.ts +4 -1
  27. package/dist/server/server/workspace-git-service.js +40 -22
  28. package/dist/server/services/github-service.d.ts +57 -0
  29. package/dist/server/services/github-service.js +327 -3
  30. package/dist/server/terminal/terminal-session-controller.d.ts +6 -0
  31. package/dist/server/terminal/terminal-session-controller.js +36 -2
  32. package/dist/src/server/persisted-config.js +2 -0
  33. package/package.json +5 -5
@@ -65,9 +65,13 @@ export interface AgentManagerOptions {
65
65
  idFactory?: () => string;
66
66
  registry?: AgentStorage;
67
67
  onAgentAttention?: AgentAttentionCallback;
68
+ onWorkspaceStateMayHaveChanged?: (params: {
69
+ cwd: string;
70
+ }) => void;
68
71
  durableTimelineStore?: AgentTimelineStore;
69
72
  terminalManager?: TerminalManager | null;
70
73
  mcpBaseUrl?: string;
74
+ mcpAuthToken?: string;
71
75
  appendSystemPrompt?: string;
72
76
  agentStreamCoalesceWindowMs?: number;
73
77
  rescueTimeouts?: AgentManagerRescueTimeouts;
@@ -179,9 +183,11 @@ export declare class AgentManager {
179
183
  private readonly backgroundTasks;
180
184
  private readonly agentStreamCoalescer;
181
185
  private mcpBaseUrl;
186
+ private readonly mcpAuthToken;
182
187
  private appendSystemPrompt;
183
188
  private onAgentAttention?;
184
189
  private onAgentArchived?;
190
+ private onWorkspaceStateMayHaveChanged?;
185
191
  private logger;
186
192
  private readonly rescueTimeouts;
187
193
  constructor(options: AgentManagerOptions);
@@ -194,6 +200,13 @@ export declare class AgentManager {
194
200
  setAgentAttentionCallback(callback: AgentAttentionCallback): void;
195
201
  setAgentArchivedCallback(callback: AgentArchivedCallback): void;
196
202
  setMcpBaseUrl(url: string | null): void;
203
+ /**
204
+ * Capability token the daemon's own MCP clients must present to the Agent MCP
205
+ * endpoint when a daemon password is configured. Read by the per-client
206
+ * session to authenticate its own MCP connection. Stays in the daemon — never
207
+ * sent to remote clients.
208
+ */
209
+ getMcpAuthToken(): string | null;
197
210
  setAppendSystemPrompt(prompt: string | null | undefined): void;
198
211
  getMetricsSnapshot(): AgentMetricsSnapshot;
199
212
  private touchUpdatedAt;
@@ -355,4 +368,5 @@ export declare class AgentManager {
355
368
  private requireAgent;
356
369
  private requireSessionAgent;
357
370
  }
371
+ export declare function commandMayHaveChangedExternalState(command: string): boolean;
358
372
  //# sourceMappingURL=agent-manager.d.ts.map
@@ -177,7 +177,9 @@ export class AgentManager {
177
177
  this.registry = options?.registry;
178
178
  this.durableTimelineStore = options?.durableTimelineStore;
179
179
  this.onAgentAttention = options?.onAgentAttention;
180
+ this.onWorkspaceStateMayHaveChanged = options?.onWorkspaceStateMayHaveChanged;
180
181
  this.mcpBaseUrl = options?.mcpBaseUrl ?? null;
182
+ this.mcpAuthToken = options?.mcpAuthToken ?? null;
181
183
  this.appendSystemPrompt = options.appendSystemPrompt ?? "";
182
184
  this.logger = options.logger.child({ module: "agent", component: "agent-manager" });
183
185
  this.rescueTimeouts = {
@@ -224,6 +226,15 @@ export class AgentManager {
224
226
  setMcpBaseUrl(url) {
225
227
  this.mcpBaseUrl = url;
226
228
  }
229
+ /**
230
+ * Capability token the daemon's own MCP clients must present to the Agent MCP
231
+ * endpoint when a daemon password is configured. Read by the per-client
232
+ * session to authenticate its own MCP connection. Stays in the daemon — never
233
+ * sent to remote clients.
234
+ */
235
+ getMcpAuthToken() {
236
+ return this.mcpAuthToken;
237
+ }
227
238
  setAppendSystemPrompt(prompt) {
228
239
  this.appendSystemPrompt = prompt ?? "";
229
240
  }
@@ -2277,6 +2288,15 @@ export class AgentManager {
2277
2288
  epoch: this.timelineStore.getEpoch(agentId),
2278
2289
  timestamp: row.timestamp,
2279
2290
  });
2291
+ if (item.type === "tool_call" &&
2292
+ item.status === "completed" &&
2293
+ item.detail?.type === "shell" &&
2294
+ commandMayHaveChangedExternalState(item.detail.command)) {
2295
+ const agent = this.agents.get(agentId);
2296
+ if (agent) {
2297
+ this.onWorkspaceStateMayHaveChanged?.({ cwd: agent.cwd });
2298
+ }
2299
+ }
2280
2300
  return event;
2281
2301
  }
2282
2302
  async appendSystemErrorTimelineMessage(agent, provider, message, options) {
@@ -2536,6 +2556,7 @@ export class AgentManager {
2536
2556
  config: storedConfig,
2537
2557
  agentId,
2538
2558
  mcpBaseUrl: this.mcpBaseUrl,
2559
+ mcpAuthToken: this.mcpAuthToken,
2539
2560
  }));
2540
2561
  return { storedConfig, launchConfig };
2541
2562
  }
@@ -2626,4 +2647,19 @@ export class AgentManager {
2626
2647
  return agent;
2627
2648
  }
2628
2649
  }
2650
+ export function commandMayHaveChangedExternalState(command) {
2651
+ const normalized = command.toLowerCase();
2652
+ // Commands that operate on remote state and do NOT trigger local file
2653
+ // watchers. Local git mutations (commit, checkout, merge, rebase, reset,
2654
+ // pull) are already caught by watchers on .git/HEAD and refs/heads/.
2655
+ return (
2656
+ // GitHub PR operations (merge, close, create, edit, comment, review)
2657
+ /\bgh\s+pr\s+(merge|close|create|edit|comment|review)\b/.test(normalized) ||
2658
+ // Pushes to remote — local refs unchanged, but remote state (PR checks,
2659
+ // mergeable status) may shift immediately after.
2660
+ /\bgit\s+push\b/.test(normalized) ||
2661
+ // Fetches update refs/remotes/ which our watchers do not watch, so
2662
+ // ahead/behind counts can drift stale until the next refresh.
2663
+ /\bgit\s+fetch\b/.test(normalized));
2664
+ }
2629
2665
  //# sourceMappingURL=agent-manager.js.map
@@ -133,6 +133,7 @@ export interface AgentFeatureSelect {
133
133
  }
134
134
  export type AgentFeature = AgentFeatureToggle | AgentFeatureSelect;
135
135
  export interface AgentCapabilityFlags {
136
+ [capability: string]: boolean | undefined;
136
137
  supportsStreaming: boolean;
137
138
  supportsSessionPersistence: boolean;
138
139
  supportsSessionListing?: boolean;
@@ -3,6 +3,7 @@ import type { Logger } from "pino";
3
3
  import { type ClaudeQueryFactory } from "./query.js";
4
4
  import { type AgentCapabilityFlags, type AgentClient, type AgentCreateSessionOptions, type AgentFeature, type AgentLaunchContext, type AgentMetadata, type AgentModelDefinition, type AgentPersistenceHandle, type AgentSession, type AgentSessionConfig, type AgentTimelineItem, type ImportableProviderSession, type ImportProviderSessionContext, type ImportProviderSessionInput, type ListImportableSessionsOptions, type ListModelsOptions, type McpServerConfig } from "../../agent-sdk-types.js";
5
5
  import { type ProviderRuntimeSettings } from "../../provider-launch-config.js";
6
+ export declare function normalizeClaudeAskUserQuestionRequestInput(toolName: string, input: AgentMetadata): AgentMetadata;
6
7
  export declare function normalizeClaudeAskUserQuestionUpdatedInput(updatedInput: AgentMetadata | undefined, fallbackInput: AgentMetadata | undefined): AgentMetadata;
7
8
  interface EventIdentifiers {
8
9
  taskId: string | null;
@@ -31,6 +31,42 @@ const CLAUDE_SETTING_SOURCES = [
31
31
  function readNonEmptyString(value) {
32
32
  return typeof value === "string" && value.trim().length > 0 ? value : null;
33
33
  }
34
+ export function normalizeClaudeAskUserQuestionRequestInput(toolName, input) {
35
+ if (toolName !== "AskUserQuestion" || !Array.isArray(input.questions)) {
36
+ return input;
37
+ }
38
+ // Claude Code's AskUserQuestion schema says "Other" is host-provided, not a
39
+ // model-supplied option. Paseo's shared question UI uses allowOther for that
40
+ // freeform answer path.
41
+ return {
42
+ ...input,
43
+ questions: input.questions.map((item) => {
44
+ if (!isMetadata(item)) {
45
+ return item;
46
+ }
47
+ return {
48
+ ...item,
49
+ allowOther: true,
50
+ };
51
+ }),
52
+ };
53
+ }
54
+ function stripClaudeAskUserQuestionUiMetadata(input) {
55
+ if (!Array.isArray(input.questions)) {
56
+ return input;
57
+ }
58
+ return {
59
+ ...input,
60
+ questions: input.questions.map((item) => {
61
+ if (!isMetadata(item) || !("allowOther" in item)) {
62
+ return item;
63
+ }
64
+ const itemForClaude = { ...item };
65
+ delete itemForClaude.allowOther;
66
+ return itemForClaude;
67
+ }),
68
+ };
69
+ }
34
70
  export function normalizeClaudeAskUserQuestionUpdatedInput(updatedInput, fallbackInput) {
35
71
  const fallback = isMetadata(fallbackInput) ? fallbackInput : {};
36
72
  const base = isMetadata(updatedInput) ? updatedInput : {};
@@ -38,7 +74,7 @@ export function normalizeClaudeAskUserQuestionUpdatedInput(updatedInput, fallbac
38
74
  // AskUserQuestion tool expects answer keys to match the full question text. Merge
39
75
  // the original request payload back in so provider callbacks that only return
40
76
  // `{ answers }` still satisfy Claude's full tool input schema.
41
- const merged = { ...fallback, ...base };
77
+ const merged = stripClaudeAskUserQuestionUiMetadata({ ...fallback, ...base });
42
78
  const questions = (Array.isArray(base.questions) ? base.questions : null) ??
43
79
  (Array.isArray(fallback.questions) ? fallback.questions : null);
44
80
  const answers = isMetadata(base.answers) ? base.answers : null;
@@ -131,10 +167,28 @@ const REWIND_COMMAND = {
131
167
  description: "Rewind tracked files to a previous user message",
132
168
  argumentHint: "[user_message_uuid]",
133
169
  };
170
+ const CLAUDE_ROOT_ONLY_COMMANDS = new Set([
171
+ "clear",
172
+ "compact",
173
+ "context",
174
+ "debug",
175
+ "extra-usage",
176
+ "heapdump",
177
+ "init",
178
+ "loop",
179
+ "schedule",
180
+ "usage",
181
+ ]);
134
182
  const INTERRUPT_TOOL_USE_PLACEHOLDER = "[Request interrupted by user for tool use]";
135
183
  const INTERRUPT_PLACEHOLDER_PATTERN = /^\[Request interrupted by user(?:[^\]]*)\]$/;
136
184
  const NO_RESPONSE_REQUESTED_PLACEHOLDER = "No response requested.";
137
185
  const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
186
+ function classifyClaudeSlashCommand(commandName) {
187
+ // Claude exposes commands and skills as one flat SDK list, without structured source
188
+ // metadata. Keep obvious root-only/session controls out of inline autocomplete and
189
+ // treat the rest as skills; the worst failure mode is an inert inline suggestion.
190
+ return CLAUDE_ROOT_ONLY_COMMANDS.has(commandName) ? "command" : "skill";
191
+ }
138
192
  function resolvePathEnvKey() {
139
193
  if (process.env["Path"] !== undefined)
140
194
  return "Path";
@@ -1204,6 +1258,7 @@ class ClaudeAgentSession {
1204
1258
  this.handlePermissionRequest = async (toolName, input, options) => {
1205
1259
  const requestId = `permission-${randomUUID()}`;
1206
1260
  const kind = resolvePermissionKind(toolName, input);
1261
+ const requestInput = normalizeClaudeAskUserQuestionRequestInput(toolName, input);
1207
1262
  const metadata = {};
1208
1263
  if (options.toolUseID) {
1209
1264
  metadata.toolUseId = options.toolUseID;
@@ -1224,7 +1279,7 @@ class ClaudeAgentSession {
1224
1279
  provider: "claude",
1225
1280
  name: toolName,
1226
1281
  kind,
1227
- input,
1282
+ input: requestInput,
1228
1283
  detail: toolDetail,
1229
1284
  suggestions: options.suggestions?.map((suggestion) => ({
1230
1285
  ...suggestion,
@@ -1665,7 +1720,7 @@ class ClaudeAgentSession {
1665
1720
  name: cmd.name,
1666
1721
  description: cmd.description,
1667
1722
  argumentHint: cmd.argumentHint,
1668
- kind: "command",
1723
+ kind: classifyClaudeSlashCommand(cmd.name),
1669
1724
  });
1670
1725
  }
1671
1726
  }
@@ -15,6 +15,13 @@ const CLAUDE_OPUS_EXTENDED_THINKING_OPTIONS = [
15
15
  { id: "max", label: "Max" },
16
16
  ];
17
17
  const CLAUDE_MODELS = [
18
+ {
19
+ provider: "claude",
20
+ id: "claude-fable-5",
21
+ label: "Fable 5",
22
+ description: "Fable 5 · Most powerful model",
23
+ thinkingOptions: [...CLAUDE_OPUS_EXTENDED_THINKING_OPTIONS],
24
+ },
18
25
  {
19
26
  provider: "claude",
20
27
  id: "claude-opus-4-8[1m]",
@@ -170,6 +177,14 @@ export function normalizeClaudeRuntimeModelId(value) {
170
177
  if (CLAUDE_MODELS.some((model) => model.id === trimmed)) {
171
178
  return trimmed;
172
179
  }
180
+ // Fable uses a single-segment version (claude-fable-5), not the {major}-{minor}
181
+ // scheme of opus/sonnet/haiku, so match it separately. This maps dated runtime
182
+ // strings (e.g. claude-fable-5-20260301) back to the catalog ID. No [1m] variant:
183
+ // Fable 5 is natively 1M, so there is no 200K-default model to opt into 1M.
184
+ const fableMatch = trimmed.match(/(?:claude-)?fable[-_ ]+(\d+)/i);
185
+ if (fableMatch) {
186
+ return `claude-fable-${fableMatch[1]}`;
187
+ }
173
188
  // Match: claude-{family}-{major}-{minor}[1m]? possibly followed by a date suffix
174
189
  const runtimeMatch = trimmed.match(/(?:claude-)?(opus|sonnet|haiku)[-_ ]+(\d+)[-.](\d+)(\[1m\])?/i);
175
190
  if (!runtimeMatch) {
@@ -10,15 +10,15 @@ declare const TaskNotificationEnvelopeSchema: z.ZodObject<{
10
10
  }, "strip", z.ZodTypeAny, {
11
11
  status: string | null;
12
12
  messageId: string | null;
13
- taskId: string | null;
14
13
  summary: string | null;
14
+ taskId: string | null;
15
15
  outputFile: string | null;
16
16
  rawText: string | null;
17
17
  }, {
18
18
  status: string | null;
19
19
  messageId: string | null;
20
- taskId: string | null;
21
20
  summary: string | null;
21
+ taskId: string | null;
22
22
  outputFile: string | null;
23
23
  rawText: string | null;
24
24
  }>;
@@ -64,8 +64,54 @@ const MODELS = [
64
64
  function shouldEmitPlanApprovalPrompt(prompt) {
65
65
  return /emit\s+(?:a\s+)?synthetic\s+plan\s+approval/i.test(promptToText(prompt));
66
66
  }
67
- function shouldEmitQuestionPrompt(prompt) {
68
- return /emit\s+(?:a\s+)?synthetic\s+questions?/i.test(promptToText(prompt));
67
+ function parseMockQuestionPrompt(prompt) {
68
+ const text = promptToText(prompt);
69
+ if (!/emit\s+(?:a\s+)?synthetic\s+questions?/i.test(text)) {
70
+ return null;
71
+ }
72
+ if (/free[-\s]?write|freeform|text[-\s]?only/i.test(text)) {
73
+ return {
74
+ questions: [
75
+ {
76
+ question: "What is the GitHub private repo URL to push to?",
77
+ header: "repoUrl",
78
+ options: [],
79
+ multiSelect: false,
80
+ placeholder: "git@github.com:user/repo.git",
81
+ },
82
+ {
83
+ question: "What should the first commit message be?",
84
+ header: "commitMessage",
85
+ options: [],
86
+ multiSelect: false,
87
+ placeholder: "Initial commit",
88
+ },
89
+ ],
90
+ };
91
+ }
92
+ return {
93
+ questions: [
94
+ {
95
+ question: "Which surface should this apply to?",
96
+ header: "surface",
97
+ options: [{ label: "App" }, { label: "Desktop" }],
98
+ multiSelect: false,
99
+ },
100
+ {
101
+ question: "Which rollout should we use?",
102
+ header: "rollout",
103
+ options: [{ label: "Immediately" }, { label: "Behind feature flag" }],
104
+ multiSelect: false,
105
+ },
106
+ {
107
+ question: "What success criteria should we use?",
108
+ header: "success",
109
+ options: [],
110
+ multiSelect: false,
111
+ placeholder: "Describe success...",
112
+ },
113
+ ],
114
+ };
69
115
  }
70
116
  function resolveModelProfile(modelId) {
71
117
  const model = MODELS.find((entry) => entry.id === modelId) ?? MODELS[0];
@@ -399,11 +445,12 @@ export class MockLoadTestAgentSession {
399
445
  }, 0);
400
446
  const largePayload = parseLargeAgentStreamPayloadPrompt(prompt);
401
447
  const stress = parseAgentStreamStressPrompt(prompt);
448
+ const questionPrompt = parseMockQuestionPrompt(prompt);
402
449
  if (shouldEmitPlanApprovalPrompt(prompt)) {
403
450
  this.schedulePlanApprovalTurn(turn);
404
451
  }
405
- else if (shouldEmitQuestionPrompt(prompt)) {
406
- this.scheduleQuestionPromptTurn(turn);
452
+ else if (questionPrompt) {
453
+ this.scheduleQuestionPromptTurn(turn, questionPrompt);
407
454
  }
408
455
  else if (largePayload) {
409
456
  this.scheduleLargePayloadTurn(turn, largePayload);
@@ -547,9 +594,9 @@ export class MockLoadTestAgentSession {
547
594
  }, 0);
548
595
  turn.timer.unref?.();
549
596
  }
550
- scheduleQuestionPromptTurn(turn) {
597
+ scheduleQuestionPromptTurn(turn, questionPrompt) {
551
598
  turn.timer = setTimeout(() => {
552
- this.emitQuestionPromptTurn(turn);
599
+ this.emitQuestionPromptTurn(turn, questionPrompt);
553
600
  }, 0);
554
601
  turn.timer.unref?.();
555
602
  }
@@ -601,7 +648,7 @@ export class MockLoadTestAgentSession {
601
648
  turnId: turn.turnId,
602
649
  });
603
650
  }
604
- emitQuestionPromptTurn(turn) {
651
+ emitQuestionPromptTurn(turn, questionPrompt) {
605
652
  if (this.activeTurn !== turn) {
606
653
  return;
607
654
  }
@@ -618,27 +665,7 @@ export class MockLoadTestAgentSession {
618
665
  kind: "question",
619
666
  title: "Questions",
620
667
  input: {
621
- questions: [
622
- {
623
- question: "Which surface should this apply to?",
624
- header: "surface",
625
- options: [{ label: "App" }, { label: "Desktop" }],
626
- multiSelect: false,
627
- },
628
- {
629
- question: "Which rollout should we use?",
630
- header: "rollout",
631
- options: [{ label: "Immediately" }, { label: "Behind feature flag" }],
632
- multiSelect: false,
633
- },
634
- {
635
- question: "What success criteria should we use?",
636
- header: "success",
637
- options: [],
638
- multiSelect: false,
639
- placeholder: "Describe success...",
640
- },
641
- ],
668
+ questions: questionPrompt.questions,
642
669
  },
643
670
  metadata: {
644
671
  source: "mock_questions",
@@ -12,7 +12,7 @@ import { buildBinaryDiagnosticRows, formatDiagnosticStatus, formatProviderDiagno
12
12
  import { getUserMessageText, streamPiHistory, } from "./history-mapper.js";
13
13
  import { PiCliRuntime } from "./cli-runtime.js";
14
14
  import { revertPiConversation } from "./rewind.js";
15
- import { listPiImportableSessions } from "./session-descriptor.js";
15
+ import { listPiImportableSessions, readPiImportSessionConfig } from "./session-descriptor.js";
16
16
  import { mapToolDetail, parseToolArgs, parseToolResult, resolveToolCallName, } from "./tool-call-mapper.js";
17
17
  const PI_PROVIDER = "pi";
18
18
  const DEFAULT_PI_THINKING_LEVEL = "medium";
@@ -1557,11 +1557,13 @@ export class PiRpcAgentClient {
1557
1557
  });
1558
1558
  }
1559
1559
  async importSession(input, context) {
1560
+ const importConfig = await readPiImportSessionConfig(input.providerHandleId);
1560
1561
  return importSessionFromPersistence({
1561
1562
  provider: PI_PROVIDER,
1562
1563
  request: input,
1563
1564
  context,
1564
1565
  resumeSession: this.resumeSession.bind(this),
1566
+ config: importConfig,
1565
1567
  });
1566
1568
  }
1567
1569
  async isAvailable() {
@@ -6,6 +6,11 @@ interface PiSessionDescriptorOptions extends ListImportableSessionsOptions {
6
6
  env?: NodeJS.ProcessEnv;
7
7
  homeDir?: string;
8
8
  }
9
+ export interface PiImportSessionConfig {
10
+ model?: string;
11
+ thinkingOptionId?: string;
12
+ }
9
13
  export declare function listPiImportableSessions(options?: PiSessionDescriptorOptions): Promise<ImportableProviderSession[]>;
14
+ export declare function readPiImportSessionConfig(filePath: string): Promise<PiImportSessionConfig>;
10
15
  export {};
11
16
  //# sourceMappingURL=session-descriptor.d.ts.map
@@ -26,6 +26,12 @@ export async function listPiImportableSessions(options = {}) {
26
26
  .sort((left, right) => right.lastActivityAt.getTime() - left.lastActivityAt.getTime())
27
27
  .slice(0, limit);
28
28
  }
29
+ export async function readPiImportSessionConfig(filePath) {
30
+ const descriptor = await readPiSessionDescriptor(filePath);
31
+ if (!descriptor)
32
+ return {};
33
+ return toPiImportSessionConfig(descriptor);
34
+ }
29
35
  async function resolvePiSessionsDir(options) {
30
36
  const env = options.env ?? process.env;
31
37
  const homeDir = options.homeDir ?? homedir();
@@ -103,6 +109,19 @@ async function walkJsonlFiles(root) {
103
109
  return files.flat();
104
110
  }
105
111
  async function readPiImportableSession(filePath) {
112
+ const descriptor = await readPiSessionDescriptor(filePath);
113
+ if (!descriptor)
114
+ return null;
115
+ return {
116
+ providerHandleId: filePath,
117
+ cwd: descriptor.cwd,
118
+ title: descriptor.title,
119
+ firstPromptPreview: normalizePromptPreview(descriptor.firstUserMessage),
120
+ lastPromptPreview: normalizePromptPreview(descriptor.lastUserMessage ?? descriptor.firstUserMessage),
121
+ lastActivityAt: descriptor.lastActivityAt,
122
+ };
123
+ }
124
+ async function readPiSessionDescriptor(filePath) {
106
125
  const firstLine = await readFirstLine(filePath);
107
126
  if (!firstLine)
108
127
  return null;
@@ -113,14 +132,23 @@ async function readPiImportableSession(filePath) {
113
132
  const tailInfo = parseSessionTail(tail);
114
133
  const headInfo = await scanSessionHead(filePath);
115
134
  const title = tailInfo.title ?? headInfo.title ?? headInfo.firstUserMessage;
135
+ const model = tailInfo.model ?? headInfo.model;
136
+ const thinkingOptionId = tailInfo.thinkingOptionId ?? headInfo.thinkingOptionId;
116
137
  const lastActivityAt = tailInfo.lastActivityAt ?? (await readFileMtime(filePath)) ?? header.createdAt ?? new Date(0);
117
138
  return {
118
- providerHandleId: filePath,
119
139
  cwd: header.cwd,
120
140
  title,
121
- firstPromptPreview: normalizePromptPreview(headInfo.firstUserMessage),
122
- lastPromptPreview: normalizePromptPreview(tailInfo.lastUserMessage ?? headInfo.firstUserMessage),
141
+ firstUserMessage: headInfo.firstUserMessage,
142
+ lastUserMessage: tailInfo.lastUserMessage,
123
143
  lastActivityAt,
144
+ model,
145
+ thinkingOptionId,
146
+ };
147
+ }
148
+ function toPiImportSessionConfig(descriptor) {
149
+ return {
150
+ ...(descriptor.model ? { model: descriptor.model } : {}),
151
+ ...(descriptor.thinkingOptionId ? { thinkingOptionId: descriptor.thinkingOptionId } : {}),
124
152
  };
125
153
  }
126
154
  async function readFirstLine(filePath) {
@@ -179,6 +207,8 @@ function parseSessionTail(tail) {
179
207
  let lastActivityAt = null;
180
208
  let fallbackTimestamp = null;
181
209
  let lastUserMessage = null;
210
+ let model = null;
211
+ let thinkingOptionId = null;
182
212
  for (let index = lines.length - 1; index >= 0; index -= 1) {
183
213
  const entry = parseJsonRecord(lines[index].trim());
184
214
  if (!entry)
@@ -186,24 +216,32 @@ function parseSessionTail(tail) {
186
216
  if (!title && entry.type === "session_info") {
187
217
  title = readNonEmptyString(entry.name);
188
218
  }
219
+ if (!model) {
220
+ model = extractModel(entry);
221
+ }
222
+ if (!thinkingOptionId) {
223
+ thinkingOptionId = extractThinkingOptionId(entry);
224
+ }
189
225
  const entryTimestamp = parseDate(entry.timestamp);
190
226
  if (!fallbackTimestamp && entryTimestamp) {
191
227
  fallbackTimestamp = entryTimestamp;
192
228
  }
193
- if (entry.type !== "message") {
229
+ if (entry.type !== "message")
194
230
  continue;
195
- }
196
231
  if (!lastActivityAt && entryTimestamp) {
197
232
  lastActivityAt = entryTimestamp;
198
233
  }
199
234
  if (!lastUserMessage && isRecord(entry.message) && entry.message.role === "user") {
200
235
  lastUserMessage = extractMessageText(entry.message.content);
201
236
  }
202
- if (title && lastActivityAt && lastUserMessage) {
203
- break;
204
- }
205
237
  }
206
- return { title, lastActivityAt: lastActivityAt ?? fallbackTimestamp, lastUserMessage };
238
+ return {
239
+ title,
240
+ lastActivityAt: lastActivityAt ?? fallbackTimestamp,
241
+ lastUserMessage,
242
+ model,
243
+ thinkingOptionId,
244
+ };
207
245
  }
208
246
  async function scanSessionHead(filePath) {
209
247
  let content;
@@ -211,10 +249,12 @@ async function scanSessionHead(filePath) {
211
249
  content = await readFile(filePath, "utf8");
212
250
  }
213
251
  catch {
214
- return { title: null, firstUserMessage: null };
252
+ return { title: null, firstUserMessage: null, model: null, thinkingOptionId: null };
215
253
  }
216
254
  let title = null;
217
255
  let firstUserMessage = null;
256
+ let model = null;
257
+ let thinkingOptionId = null;
218
258
  let lineCount = 0;
219
259
  for (const rawLine of content.split(/\r?\n/u)) {
220
260
  lineCount += 1;
@@ -224,19 +264,41 @@ async function scanSessionHead(filePath) {
224
264
  if (entry.type === "session_info") {
225
265
  title = readNonEmptyString(entry.name) ?? title;
226
266
  }
267
+ model = extractModel(entry) ?? model;
268
+ thinkingOptionId = extractThinkingOptionId(entry) ?? thinkingOptionId;
227
269
  if (!firstUserMessage && entry.type === "message" && isRecord(entry.message)) {
228
270
  if (entry.message.role === "user") {
229
271
  firstUserMessage = extractMessageText(entry.message.content);
230
272
  }
231
273
  }
232
- if (title && firstUserMessage) {
274
+ if (title && firstUserMessage && model && thinkingOptionId) {
233
275
  break;
234
276
  }
235
277
  if (lineCount >= FULL_SCAN_LINE_LIMIT && firstUserMessage) {
236
278
  break;
237
279
  }
238
280
  }
239
- return { title, firstUserMessage };
281
+ return { title, firstUserMessage, model, thinkingOptionId };
282
+ }
283
+ function extractModel(entry) {
284
+ if (entry.type === "model_change") {
285
+ return buildModelId(entry.provider, entry.modelId);
286
+ }
287
+ if (entry.type === "message" && isRecord(entry.message)) {
288
+ return buildModelId(entry.message.provider, entry.message.model);
289
+ }
290
+ return null;
291
+ }
292
+ function extractThinkingOptionId(entry) {
293
+ return entry.type === "thinking_level_change" ? readNonEmptyString(entry.thinkingLevel) : null;
294
+ }
295
+ function buildModelId(provider, modelId) {
296
+ const providerName = readNonEmptyString(provider);
297
+ const modelName = readNonEmptyString(modelId);
298
+ if (!providerName || !modelName) {
299
+ return null;
300
+ }
301
+ return `${providerName}/${modelName}`;
240
302
  }
241
303
  function parseJsonRecord(line) {
242
304
  if (!line)
@@ -4,5 +4,11 @@ export declare function withRuntimePaseoMcpServer(params: {
4
4
  config: AgentSessionConfig;
5
5
  agentId: string;
6
6
  mcpBaseUrl: string | null;
7
+ /**
8
+ * Capability token authenticating the injected connection to the daemon's
9
+ * Agent MCP endpoint. The daemon password is gated off this route, so without
10
+ * this header the agent's MCP requests are rejected when a password is set.
11
+ */
12
+ mcpAuthToken: string | null;
7
13
  }): AgentSessionConfig;
8
14
  //# sourceMappingURL=runtime-mcp-config.d.ts.map
@@ -31,6 +31,9 @@ export function withRuntimePaseoMcpServer(params) {
31
31
  [PASEO_MCP_SERVER_NAME]: {
32
32
  type: "http",
33
33
  url: `${params.mcpBaseUrl}?callerAgentId=${params.agentId}`,
34
+ ...(params.mcpAuthToken
35
+ ? { headers: { Authorization: `Bearer ${params.mcpAuthToken}` } }
36
+ : {}),
34
37
  },
35
38
  ...storedConfig.mcpServers,
36
39
  },
@@ -21,5 +21,18 @@ export declare function extractWsBearerProtocol(value: string | undefined): stri
21
21
  export declare function extractWsBearerToken(protocol: string | null): string | null;
22
22
  export declare function createRequireBearerMiddleware(auth: DaemonAuthConfig | undefined, onReject?: (context: BearerAuthRejectContext) => void): RequestHandler;
23
23
  export declare function shouldBypassBearerAuth(method: string, path: string): boolean;
24
+ /**
25
+ * Authorizes a request to the Agent MCP endpoint (/mcp/agents), which is exempt
26
+ * from the global daemon-password middleware. Accepts either the per-daemon-run
27
+ * capability token the daemon injects into its own agents' configs and MCP
28
+ * client, or a valid daemon-password bearer (so existing password-authenticated
29
+ * callers keep working). When no daemon password is configured the endpoint is
30
+ * open, matching the global middleware's behavior.
31
+ */
32
+ export declare function isAgentMcpRequestAuthorized(input: {
33
+ password: string | undefined;
34
+ capabilityToken: string | null;
35
+ authorizationHeader: string | undefined;
36
+ }): Promise<boolean>;
24
37
  export {};
25
38
  //# sourceMappingURL=auth.d.ts.map