praxis-agent 0.14.0 → 0.16.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 (33) hide show
  1. package/README.md +5 -2
  2. package/dist/application/session-service.d.ts +18 -0
  3. package/dist/application/session-service.js +180 -41
  4. package/dist/application/subagent-service.d.ts +7 -1
  5. package/dist/application/subagent-service.js +238 -53
  6. package/dist/cli/interactive.d.ts +7 -1
  7. package/dist/cli/interactive.js +77 -3
  8. package/dist/cli/protocol.d.ts +13 -0
  9. package/dist/cli/protocol.js +34 -0
  10. package/dist/cli/tui/claude-command-inventory.d.ts +1 -2
  11. package/dist/cli/tui/claude-command-inventory.js +1 -6
  12. package/dist/cli/tui/claude-style.d.ts +4 -1
  13. package/dist/cli/tui/claude-style.js +8 -3
  14. package/dist/cli/tui/slash-commands.d.ts +1 -0
  15. package/dist/cli/tui/slash-commands.js +11 -0
  16. package/dist/cli/tui/theme.d.ts +2 -0
  17. package/dist/cli/tui/theme.js +69 -0
  18. package/dist/cli.d.ts +6 -0
  19. package/dist/cli.js +95 -27
  20. package/dist/compatibility/claude/agent-color.d.ts +25 -0
  21. package/dist/compatibility/claude/agent-color.js +79 -0
  22. package/dist/compatibility/claude/fork.js +5 -1
  23. package/dist/compatibility/claude/schema.js +13 -1
  24. package/dist/extensions/claude-extensions.d.ts +19 -1
  25. package/dist/extensions/claude-extensions.js +126 -1
  26. package/dist/hooks/claude-hooks.d.ts +3 -1
  27. package/dist/hooks/claude-hooks.js +14 -0
  28. package/dist/mcp/claude-mcp-tools.d.ts +18 -0
  29. package/dist/mcp/claude-mcp-tools.js +46 -0
  30. package/dist/persistence/claude-transcript-store.js +1 -0
  31. package/dist/tools/claude-interactive-tools.d.ts +1 -0
  32. package/dist/tools/claude-interactive-tools.js +3 -0
  33. package/package.json +2 -1
package/dist/cli.js CHANGED
@@ -7,6 +7,7 @@ import { dirname, join, resolve } from 'node:path';
7
7
  import { fileURLToPath, pathToFileURL } from 'node:url';
8
8
  import { randomUUID } from 'node:crypto';
9
9
  import { ClaudeSessionService, } from './application/session-service.js';
10
+ import { agentColorMessage, parseAgentColorInput, } from './compatibility/claude/agent-color.js';
10
11
  import { ClaudeConditionalRuleResolver, ClaudeContextAssembler, } from './compatibility/claude/context.js';
11
12
  import { loadClaudeDynamicContext } from './compatibility/claude/dynamic-context.js';
12
13
  import { createClaudePrSessionFilter, filterClaudePrLinkedSessions, } from './compatibility/claude/pr-links.js';
@@ -46,7 +47,7 @@ import { WorkspaceContext } from './application/session-worktree.js';
46
47
  import { launchTmuxWorktree } from './platform/tmux-worktree.js';
47
48
  import { claudeSandboxRuntime } from './sandbox/claude-sandbox-runtime.js';
48
49
  import { claudeSandboxTempDirectory, loadClaudeSandboxSettings, } from './sandbox/claude-sandbox-settings.js';
49
- import { createErrorResult, createSuccessResult, parseCliInvocation, readStreamJsonMessages, StreamJsonOutput, } from './cli/protocol.js';
50
+ import { createErrorResult, createSuccessResult, matchHeadlessColorCommand, parseCliInvocation, readStreamJsonMessages, StreamJsonOutput, } from './cli/protocol.js';
50
51
  import { describeClaudePlugin, initClaudePlugin, installClaudePlugin, loadClaudePlugins, readPluginRegistry, setClaudePluginEnabled, uninstallClaudePlugin, updateClaudePlugin, validateClaudePlugin, } from './plugins/claude-plugin-runtime.js';
51
52
  import { addClaudeMarketplace, disableAllNativePlugins, installClaudeMarketplacePlugin, listClaudeMarketplaceAvailablePlugins, listNativePluginRecords, readClaudeKnownMarketplaces, removeClaudeMarketplace, setNativePluginEnabled, saveClaudePluginConfig, uninstallNativePlugin, updateClaudeMarketplace, updateNativePlugin, validateClaudeMarketplace, } from './plugins/claude-plugin-marketplace.js';
52
53
  import { executeClaudePluginEvalCommand, PLUGIN_EVAL_HELP, } from './plugins/claude-plugin-eval.js';
@@ -733,6 +734,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
733
734
  const runtimeEventSink = debug?.eventSink ?? eventSink;
734
735
  let provider;
735
736
  let providerForModel;
737
+ let providerForMainModel;
736
738
  const context = parseContextEnvironment(runtimeEnvironment);
737
739
  const apiKey = runtimeEnvironment.PRAXIS_API_KEY;
738
740
  const model = cli.model ?? runtimeEnvironment.PRAXIS_MODEL;
@@ -755,13 +757,18 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
755
757
  throw new Error('Provider environment is unavailable');
756
758
  }
757
759
  providerForModel = createProviderForModel(apiKey, providerEnvironment, context, cli, controls);
758
- const models = [model, ...(cli.fallbackModels ?? [])].filter((candidate, index, all) => all.indexOf(candidate) === index);
759
760
  const createProvider = providerForModel;
760
- const providers = models.map((candidate) => createProvider(candidate));
761
- provider =
762
- providers.length > 1
761
+ providerForMainModel = (primaryModel) => {
762
+ const models = [primaryModel, ...(cli.fallbackModels ?? [])].filter((candidate, index, all) => all.indexOf(candidate) === index);
763
+ const providers = models.map((candidate) => createProvider(candidate));
764
+ const selected = providers[0];
765
+ if (!selected)
766
+ throw new Error('A primary model is required');
767
+ return providers.length > 1
763
768
  ? new FallbackModelProvider({ providers })
764
- : providers[0];
769
+ : selected;
770
+ };
771
+ provider = providerForMainModel(model);
765
772
  }
766
773
  const options = {
767
774
  configRoot,
@@ -769,6 +776,10 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
769
776
  claudeVersion,
770
777
  eventSink: runtimeEventSink,
771
778
  sessionPersistence: cli.sessionPersistence,
779
+ explicitModel: interactiveModel !== undefined || controls.model !== undefined,
780
+ explicitSystemPrompt: cli.systemPrompt !== undefined,
781
+ agentInitialPromptHandledExternally: interactive,
782
+ agentSystemPromptOverridesExplicit: interactive,
772
783
  effort: cli.effort ?? 'high',
773
784
  ...(cli.maxTurns === undefined ? {} : { maxModelTurns: cli.maxTurns }),
774
785
  ...(cli.brief && !cli.disallowedTools.includes('SendUserMessage')
@@ -908,6 +919,15 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
908
919
  }
909
920
  }
910
921
  settings.push(...pluginResources.settings);
922
+ const configuredAgent = [...settings]
923
+ .reverse()
924
+ .map((resource) => resource.value &&
925
+ typeof resource.value === 'object' &&
926
+ !Array.isArray(resource.value)
927
+ ? resource.value.agent
928
+ : undefined)
929
+ .find((value) => typeof value === 'string' && value.length > 0);
930
+ const selectedMainAgent = agent ?? configuredAgent;
911
931
  const resources = {
912
932
  ...loadedResources,
913
933
  commands: [...loadedResources.commands, ...pluginResources.commands],
@@ -1196,10 +1216,14 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1196
1216
  ...options,
1197
1217
  provider: hostedToolProvider,
1198
1218
  ...(providerForModel ? { providerForModel } : {}),
1219
+ ...(providerForMainModel ? { providerForMainModel } : {}),
1199
1220
  tools: filteredTools,
1200
1221
  mcp: mcpTools,
1201
1222
  permissions,
1202
1223
  permissionResolverForMode,
1224
+ permissionMode: cli.dangerouslySkipPermissions
1225
+ ? 'bypassPermissions'
1226
+ : cli.permissionMode,
1203
1227
  persistPermissionUpdates: (updates) => persistTuiPermissionUpdates({
1204
1228
  cwd: workspace.cwd(),
1205
1229
  configRoot,
@@ -1220,7 +1244,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1220
1244
  : {}),
1221
1245
  ...(interactiveTools ? { interactiveTools } : {}),
1222
1246
  ...(hooks ? { hooks } : {}),
1223
- ...(agent ? { agent } : {}),
1247
+ ...(selectedMainAgent ? { agent: selectedMainAgent } : {}),
1224
1248
  contextAssembler: new ClaudeContextAssembler({
1225
1249
  loadResources: loadContextResources,
1226
1250
  loadDynamicContext: (runtimeCwd = workspace.cwd()) => loadClaudeDynamicContext({
@@ -1320,6 +1344,8 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1320
1344
  retryRecentlyDenied: (sessionId, display, retrySignal) => service.retryRecentlyDenied(sessionId, display, retrySignal),
1321
1345
  answerSideQuestion: (sessionId, question, sideSignal, onDelta, permissionMode) => service.answerSideQuestion(sessionId, question, sideSignal, onDelta, permissionMode),
1322
1346
  recordBtwUsage: (sessionId, permissionMode) => service.recordBtwUsage(sessionId, permissionMode),
1347
+ recordColorUsage: (sessionId, selection, display, permissionMode, options) => service.recordColorUsage(sessionId, selection, display, permissionMode, options),
1348
+ agentColor: (sessionId) => service.readEffectiveAgentColor(sessionId),
1323
1349
  recordBackgroundUsage: (sessionId, permissionMode) => service.recordBackgroundUsage(sessionId, permissionMode),
1324
1350
  recordBackgroundLaunch: (sessionId) => service.recordBackgroundLaunch(sessionId),
1325
1351
  forkSideQuestion: (sessionId, question, sideSignal) => service.forkSideQuestion(sessionId, question, sideSignal),
@@ -1373,6 +1399,9 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1373
1399
  taskSnapshots: (sessionId) => service.taskSnapshots(sessionId),
1374
1400
  stopTask: (sessionId, taskId) => service.stopTask(sessionId, taskId),
1375
1401
  agentDefinitions: () => extensions.agentDefinitions(),
1402
+ initialAgentPrompt: () => selectedMainAgent
1403
+ ? extensions.agent(selectedMainAgent)?.initialPrompt
1404
+ : undefined,
1376
1405
  inspect: (sessionId) => service.inspect(sessionId),
1377
1406
  export: (sessionId) => service.export(sessionId),
1378
1407
  transcript: (sessionId) => service.transcript(sessionId, pendingResumeSessionAt),
@@ -1407,7 +1436,10 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1407
1436
  runtimeInfo: () => ({
1408
1437
  ...runtimeInfo,
1409
1438
  cwd: workspace.cwd(),
1410
- model: provider?.model ?? process.env.PRAXIS_MODEL ?? 'unknown',
1439
+ model: service.model() ??
1440
+ provider?.model ??
1441
+ runtimeEnvironment.PRAXIS_MODEL ??
1442
+ 'unknown',
1411
1443
  }),
1412
1444
  promptSuggestion: (sessionId, suggestionSignal) => service.promptSuggestion(sessionId, suggestionSignal),
1413
1445
  };
@@ -3706,6 +3738,7 @@ async function execute(argv, io, dependencies, signal) {
3706
3738
  const pendingEvents = [];
3707
3739
  let streamIterator;
3708
3740
  let firstStreamMessage;
3741
+ let streamInputExhausted = false;
3709
3742
  const queuedStreamUsers = [];
3710
3743
  const earlyControlResponses = new Map();
3711
3744
  let currentTurnAbort;
@@ -3905,6 +3938,19 @@ async function execute(argv, io, dependencies, signal) {
3905
3938
  }
3906
3939
  return 0;
3907
3940
  }
3941
+ const headlessTurnReached = invocation.rewindFiles === undefined &&
3942
+ !['sessions', 'fork', 'inspect', 'export'].includes(command ?? 'run');
3943
+ if (streamIterator && headlessTurnReached) {
3944
+ const first = await nextStreamUser();
3945
+ if (first)
3946
+ firstStreamMessage = first;
3947
+ else
3948
+ streamInputExhausted = true;
3949
+ }
3950
+ const firstHeadlessPrompt = firstStreamMessage?.prompt ?? headlessPrompt;
3951
+ const firstTurnIsLocalColor = !invocation.disableSlashCommands &&
3952
+ firstHeadlessPrompt !== undefined &&
3953
+ matchHeadlessColorCommand(firstHeadlessPrompt) !== undefined;
3908
3954
  const service = await dependencies.createService({
3909
3955
  eventSink: outputFormat === 'stream-json' && !invocation.legacyJson
3910
3956
  ? (event) => {
@@ -3929,8 +3975,7 @@ async function execute(argv, io, dependencies, signal) {
3929
3975
  }
3930
3976
  }
3931
3977
  : eventSink(io, outputFormat, invocation.legacyJson),
3932
- requireProvider: invocation.rewindFiles === undefined &&
3933
- !['fork', 'sessions', 'inspect', 'export'].includes(command ?? 'run'),
3978
+ requireProvider: !streamInputExhausted && !firstTurnIsLocalColor && headlessTurnReached,
3934
3979
  ...(retryInterruptedTools ? { approveRecovery: () => true } : {}),
3935
3980
  ...(streamIterator ? { approveTool: approveStreamTool } : {}),
3936
3981
  ...(streamIterator ? { onElicitation: respondStreamElicitation } : {}),
@@ -4038,12 +4083,8 @@ async function execute(argv, io, dependencies, signal) {
4038
4083
  if (outputFormat === 'stream-json' && !invocation.legacyJson) {
4039
4084
  streamOutput = new StreamJsonOutput((value) => writeJson(io, value), runtimeInfo, activeSessionId, includePartialMessages, invocation.includeHookEvents);
4040
4085
  }
4041
- if (streamIterator) {
4042
- const first = await nextStreamUser();
4043
- if (!first)
4044
- return 0;
4045
- firstStreamMessage = first;
4046
- }
4086
+ if (streamInputExhausted)
4087
+ return 0;
4047
4088
  const initialPrompt = firstStreamMessage?.prompt ??
4048
4089
  promptFrom(command === 'resume'
4049
4090
  ? args.slice(2)
@@ -4070,15 +4111,38 @@ async function execute(argv, io, dependencies, signal) {
4070
4111
  streamOutput.replayUser(streamMessage.message);
4071
4112
  }
4072
4113
  let result;
4114
+ let colorArgs;
4073
4115
  try {
4074
- result =
4075
- existingSessionId !== undefined || !isFirstTurn
4076
- ? runSignal
4077
- ? await service.resume(activeSessionId, prompt, runSignal, undefined, streamMessage?.images, streamMessage?.documents)
4078
- : await service.resume(activeSessionId, prompt, undefined, undefined, streamMessage?.images, streamMessage?.documents)
4079
- : runSignal
4080
- ? await service.run(prompt, runSignal, activeSessionId, undefined, streamMessage?.images, streamMessage?.documents)
4081
- : await service.run(prompt, undefined, activeSessionId, undefined, streamMessage?.images, streamMessage?.documents);
4116
+ colorArgs = invocation.disableSlashCommands
4117
+ ? undefined
4118
+ : matchHeadlessColorCommand(prompt);
4119
+ if (colorArgs !== undefined) {
4120
+ if (!service.recordColorUsage) {
4121
+ throw new Error('Session color is unavailable.');
4122
+ }
4123
+ const selection = parseAgentColorInput(colorArgs);
4124
+ const localSessionId = await service.recordColorUsage(activeSessionId, selection, prompt, invocation.permissionMode, isFirstTurn && existingSessionId === undefined
4125
+ ? { createSession: true }
4126
+ : undefined);
4127
+ result = {
4128
+ sessionId: localSessionId,
4129
+ text: agentColorMessage(selection),
4130
+ usage: { inputTokens: 0, outputTokens: 0 },
4131
+ durationApiMs: 0,
4132
+ costUsd: 0,
4133
+ modelUsage: {},
4134
+ };
4135
+ }
4136
+ else {
4137
+ result =
4138
+ existingSessionId !== undefined || !isFirstTurn
4139
+ ? runSignal
4140
+ ? await service.resume(activeSessionId, prompt, runSignal, undefined, streamMessage?.images, streamMessage?.documents)
4141
+ : await service.resume(activeSessionId, prompt, undefined, undefined, streamMessage?.images, streamMessage?.documents)
4142
+ : runSignal
4143
+ ? await service.run(prompt, runSignal, activeSessionId, undefined, streamMessage?.images, streamMessage?.documents)
4144
+ : await service.run(prompt, undefined, activeSessionId, undefined, streamMessage?.images, streamMessage?.documents);
4145
+ }
4082
4146
  }
4083
4147
  catch (error) {
4084
4148
  if (isCancellation(error, turnAbort.signal)) {
@@ -4105,16 +4169,20 @@ async function execute(argv, io, dependencies, signal) {
4105
4169
  return false;
4106
4170
  }
4107
4171
  activeSessionId = result.sessionId;
4108
- if (streamOutput)
4172
+ if (streamOutput) {
4173
+ if (colorArgs !== undefined) {
4174
+ streamOutput.syntheticAssistant(result.text);
4175
+ }
4109
4176
  streamOutput.result(result, startedAt);
4177
+ }
4110
4178
  else if (outputFormat === 'json') {
4111
4179
  const resultRuntimeInfo = service.runtimeInfo?.() ?? runtimeInfo;
4112
- writeJson(io, createSuccessResult(result, resultRuntimeInfo, startedAt, Math.max(1, jsonModelTurns)));
4180
+ writeJson(io, createSuccessResult(result, resultRuntimeInfo, startedAt, colorArgs !== undefined ? 0 : Math.max(1, jsonModelTurns)));
4113
4181
  }
4114
4182
  else if (outputFormat !== 'text')
4115
4183
  writeJson(io, { type: 'result', ...result });
4116
4184
  else
4117
- io.stdout('\n');
4185
+ io.stdout(colorArgs !== undefined ? `${result.text}\n` : '\n');
4118
4186
  if (streamOutput && invocation.promptSuggestions) {
4119
4187
  try {
4120
4188
  const suggestion = await service.promptSuggestion?.(activeSessionId, runSignal);
@@ -0,0 +1,25 @@
1
+ import type { ClaudeTranscriptEntry } from './schema.js';
2
+ export declare const AGENT_COLORS: readonly ["red", "blue", "green", "yellow", "purple", "orange", "pink", "cyan"];
3
+ export type AgentColorName = (typeof AGENT_COLORS)[number];
4
+ export declare const AGENT_COLOR_DEFAULT: "default";
5
+ export type AgentColorValue = AgentColorName | typeof AGENT_COLOR_DEFAULT;
6
+ export declare const AGENT_COLOR_VALUES: readonly AgentColorValue[];
7
+ export declare const AGENT_COLOR_CHOICES: string;
8
+ export declare const RESET_AGENT_COLOR_ALIASES: readonly ["default", "reset", "none", "gray", "grey"];
9
+ export declare function isAgentColorName(value: unknown): value is AgentColorName;
10
+ export declare function isAgentColorValue(value: unknown): value is AgentColorValue;
11
+ export declare function normalizeAgentColorInput(input: string): string;
12
+ export type AgentColorSelection = {
13
+ kind: 'color';
14
+ color: AgentColorName;
15
+ } | {
16
+ kind: 'reset';
17
+ } | {
18
+ kind: 'invalid';
19
+ input: string;
20
+ };
21
+ export declare function parseAgentColorInput(input: string): AgentColorSelection;
22
+ export declare function randomAgentColor(): AgentColorName;
23
+ export declare function agentColorMessage(selection: AgentColorSelection): string;
24
+ export declare function getClaudeEffectiveAgentColor(entries: readonly ClaudeTranscriptEntry[], sessionId: string): AgentColorName | undefined;
25
+ //# sourceMappingURL=agent-color.d.ts.map
@@ -0,0 +1,79 @@
1
+ export const AGENT_COLORS = [
2
+ 'red',
3
+ 'blue',
4
+ 'green',
5
+ 'yellow',
6
+ 'purple',
7
+ 'orange',
8
+ 'pink',
9
+ 'cyan',
10
+ ];
11
+ export const AGENT_COLOR_DEFAULT = 'default';
12
+ export const AGENT_COLOR_VALUES = [
13
+ ...AGENT_COLORS,
14
+ AGENT_COLOR_DEFAULT,
15
+ ];
16
+ export const AGENT_COLOR_CHOICES = `${AGENT_COLORS.join(', ')}, ${AGENT_COLOR_DEFAULT}`;
17
+ export const RESET_AGENT_COLOR_ALIASES = [
18
+ AGENT_COLOR_DEFAULT,
19
+ 'reset',
20
+ 'none',
21
+ 'gray',
22
+ 'grey',
23
+ ];
24
+ export function isAgentColorName(value) {
25
+ return (typeof value === 'string' &&
26
+ AGENT_COLORS.includes(value));
27
+ }
28
+ export function isAgentColorValue(value) {
29
+ return (typeof value === 'string' &&
30
+ AGENT_COLOR_VALUES.includes(value));
31
+ }
32
+ export function normalizeAgentColorInput(input) {
33
+ return input.trim().toLowerCase();
34
+ }
35
+ export function parseAgentColorInput(input) {
36
+ const normalized = normalizeAgentColorInput(input);
37
+ if (normalized === '') {
38
+ return { kind: 'color', color: randomAgentColor() };
39
+ }
40
+ if (RESET_AGENT_COLOR_ALIASES.includes(normalized)) {
41
+ return { kind: 'reset' };
42
+ }
43
+ if (isAgentColorName(normalized)) {
44
+ return { kind: 'color', color: normalized };
45
+ }
46
+ return { kind: 'invalid', input: normalized };
47
+ }
48
+ export function randomAgentColor() {
49
+ const index = Math.floor(Math.random() * AGENT_COLORS.length);
50
+ const color = AGENT_COLORS[index];
51
+ if (color === undefined)
52
+ throw new Error('AGENT_COLORS must not be empty');
53
+ return color;
54
+ }
55
+ export function agentColorMessage(selection) {
56
+ switch (selection.kind) {
57
+ case 'color':
58
+ return `Session color set to: ${selection.color}`;
59
+ case 'reset':
60
+ return 'Session color reset to default';
61
+ case 'invalid':
62
+ return `Invalid color "${selection.input}". Available colors: ${AGENT_COLOR_CHOICES}`;
63
+ }
64
+ }
65
+ export function getClaudeEffectiveAgentColor(entries, sessionId) {
66
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
67
+ const entry = entries[index];
68
+ if (entry?.type !== 'agent-color' || entry.sessionId !== sessionId) {
69
+ continue;
70
+ }
71
+ const color = entry.agentColor;
72
+ if (color === AGENT_COLOR_DEFAULT)
73
+ return undefined;
74
+ if (isAgentColorName(color))
75
+ return color;
76
+ }
77
+ return undefined;
78
+ }
79
+ //# sourceMappingURL=agent-color.js.map
@@ -152,6 +152,7 @@ function validateNativeHistory(entries) {
152
152
  return undefined;
153
153
  }
154
154
  export function createClaudeNativeFork({ source, sourceSessionId, sessionId, resumeSessionAt, }) {
155
+ const agentColors = [];
155
156
  const titles = [];
156
157
  const modes = [];
157
158
  const permissionModes = [];
@@ -204,7 +205,9 @@ export function createClaudeNativeFork({ source, sourceSessionId, sessionId, res
204
205
  throw new Error('Claude fork source entry has the wrong sessionId');
205
206
  }
206
207
  const copied = copyClaudeEntryWithSessionId(entry, sessionId);
207
- if (entry.type === 'ai-title')
208
+ if (entry.type === 'agent-color')
209
+ agentColors.push(copied);
210
+ else if (entry.type === 'ai-title')
208
211
  titles.push(copied);
209
212
  else if (entry.type === 'mode')
210
213
  modes.push(copied);
@@ -232,6 +235,7 @@ export function createClaudeNativeFork({ source, sourceSessionId, sessionId, res
232
235
  lastPrompt = undefined;
233
236
  }
234
237
  return [
238
+ ...agentColors.slice(-1),
235
239
  ...titles.slice(-1),
236
240
  ...modes.slice(-1),
237
241
  ...permissionModes.slice(-1),
@@ -1,5 +1,7 @@
1
+ import { isAgentColorValue } from './agent-color.js';
1
2
  const SUPPORTED_VERSION = '2.1.208';
2
3
  const APPENDABLE_ENTRY_TYPES = new Set([
4
+ 'agent-color',
3
5
  'agent-name',
4
6
  'agent-setting',
5
7
  'assistant',
@@ -17,6 +19,7 @@ const APPENDABLE_ENTRY_TYPES = new Set([
17
19
  'worktree-state',
18
20
  ]);
19
21
  const FORKABLE_ENTRY_TYPES = new Set([
22
+ 'agent-color',
20
23
  'agent-name',
21
24
  'agent-setting',
22
25
  'ai-title',
@@ -717,6 +720,13 @@ function validateAppendableEntry(entry) {
717
720
  }
718
721
  return;
719
722
  }
723
+ if (entry.type === 'agent-color') {
724
+ if (!isAgentColorValue(entry.agentColor) ||
725
+ !isNonEmptyString(entry.sessionId)) {
726
+ throw new Error('Claude agent-color entry has invalid metadata');
727
+ }
728
+ return;
729
+ }
720
730
  if (entry.type === 'agent-name') {
721
731
  if (!isNonEmptyString(entry.agentName) ||
722
732
  !isNonEmptyString(entry.sessionId)) {
@@ -868,7 +878,9 @@ function validateSidechainEntry(entry) {
868
878
  }
869
879
  }
870
880
  function validateForkableEntry(entry) {
871
- if (entry.type === 'custom-title' || entry.type === 'agent-name') {
881
+ if (entry.type === 'custom-title' ||
882
+ entry.type === 'agent-name' ||
883
+ entry.type === 'agent-color') {
872
884
  validateAppendableEntry(entry);
873
885
  return;
874
886
  }
@@ -23,6 +23,24 @@ export interface ClaudeAgentDefinition {
23
23
  name: string;
24
24
  description: string;
25
25
  }
26
+ export type ClaudeAgentEffort = 'low' | 'medium' | 'high' | 'max' | number;
27
+ export type ClaudeAgentPermissionMode = 'acceptEdits' | 'auto' | 'bypassPermissions' | 'default' | 'dontAsk' | 'plan';
28
+ export interface ClaudeAgentRuntimeDefinition extends ClaudeExtensionDefinition {
29
+ kind: 'agent';
30
+ tools?: readonly string[];
31
+ disallowedTools?: readonly string[];
32
+ model?: string;
33
+ effort?: ClaudeAgentEffort;
34
+ permissionMode?: ClaudeAgentPermissionMode;
35
+ maxTurns?: number;
36
+ skills?: readonly string[];
37
+ initialPrompt?: string;
38
+ memory?: 'user' | 'project' | 'local';
39
+ background?: boolean;
40
+ isolation?: 'worktree';
41
+ mcpServers?: readonly unknown[];
42
+ hooks?: Readonly<Record<string, unknown>>;
43
+ }
26
44
  export interface ClaudePromptExpansion {
27
45
  userMessages: readonly string[];
28
46
  messages?: readonly ClaudePromptExpansionMessage[];
@@ -50,7 +68,7 @@ export declare class ClaudeExtensionCatalog {
50
68
  slashCommandDefinitions(): readonly ClaudeSlashCommandDefinition[];
51
69
  skill(name: string): ClaudeExtensionDefinition | null;
52
70
  modelInvocableSkills(): readonly ClaudeExtensionDefinition[];
53
- agent(name: string): ClaudeExtensionDefinition | null;
71
+ agent(name: string): ClaudeAgentRuntimeDefinition | null;
54
72
  agentNames(): readonly string[];
55
73
  agentDefinitions(): readonly ClaudeAgentDefinition[];
56
74
  agentMentionMessages(prompt: string): readonly string[];
@@ -122,6 +122,123 @@ function parseFrontmatter(resource) {
122
122
  body: lines.slice(closingIndex + 1).join('\n'),
123
123
  };
124
124
  }
125
+ function parseAgentList(value) {
126
+ if (value === undefined)
127
+ return undefined;
128
+ const values = typeof value === 'string'
129
+ ? [value]
130
+ : Array.isArray(value)
131
+ ? value.filter((item) => typeof item === 'string')
132
+ : [];
133
+ const parsed = [];
134
+ for (const value of values) {
135
+ let current = '';
136
+ let depth = 0;
137
+ for (const character of value) {
138
+ if (character === '(')
139
+ depth += 1;
140
+ if (character === ')' && depth > 0)
141
+ depth -= 1;
142
+ if (depth === 0 && (character === ',' || /\s/u.test(character))) {
143
+ if (current.trim())
144
+ parsed.push(current.trim());
145
+ current = '';
146
+ }
147
+ else {
148
+ current += character;
149
+ }
150
+ }
151
+ if (current.trim())
152
+ parsed.push(current.trim());
153
+ }
154
+ return parsed;
155
+ }
156
+ function parseAgentEffort(value) {
157
+ if (typeof value === 'number') {
158
+ return Number.isInteger(value) ? value : undefined;
159
+ }
160
+ if (typeof value !== 'string' || !value.trim())
161
+ return undefined;
162
+ const normalized = value.trim().toLowerCase();
163
+ if (['low', 'medium', 'high', 'max'].includes(normalized)) {
164
+ return normalized;
165
+ }
166
+ if (!/^[+-]?\d+$/u.test(normalized))
167
+ return undefined;
168
+ const numeric = Number(normalized);
169
+ return Number.isInteger(numeric) ? numeric : undefined;
170
+ }
171
+ function parsePositiveInteger(value) {
172
+ if (typeof value === 'number') {
173
+ return Number.isSafeInteger(value) && value > 0 ? value : undefined;
174
+ }
175
+ if (typeof value !== 'string' || !/^[1-9]\d*$/u.test(value)) {
176
+ return undefined;
177
+ }
178
+ const numeric = Number(value);
179
+ return Number.isSafeInteger(numeric) ? numeric : undefined;
180
+ }
181
+ function parseAgentDefinition(resource) {
182
+ const definition = parseDefinition('agent', resource);
183
+ const { metadata } = parseFrontmatter(resource);
184
+ const parsedTools = parseAgentList(metadata.tools);
185
+ const tools = parsedTools?.includes('*') ? undefined : parsedTools;
186
+ const disallowedTools = parseAgentList(metadata.disallowedTools);
187
+ const skills = parseAgentList(metadata.skills);
188
+ const model = typeof metadata.model === 'string' && metadata.model.trim()
189
+ ? metadata.model.trim().toLowerCase() === 'inherit'
190
+ ? 'inherit'
191
+ : metadata.model.trim()
192
+ : undefined;
193
+ const effort = parseAgentEffort(metadata.effort);
194
+ const permissionMode = [
195
+ 'acceptEdits',
196
+ 'auto',
197
+ 'bypassPermissions',
198
+ 'default',
199
+ 'dontAsk',
200
+ 'plan',
201
+ ].includes(String(metadata.permissionMode))
202
+ ? metadata.permissionMode
203
+ : undefined;
204
+ const maxTurns = parsePositiveInteger(metadata.maxTurns);
205
+ const initialPrompt = typeof metadata.initialPrompt === 'string' && metadata.initialPrompt.trim()
206
+ ? metadata.initialPrompt
207
+ : undefined;
208
+ const memory = ['user', 'project', 'local'].includes(String(metadata.memory))
209
+ ? metadata.memory
210
+ : undefined;
211
+ const background = metadata.background === true || metadata.background === 'true'
212
+ ? true
213
+ : undefined;
214
+ const isolation = metadata.isolation === 'worktree' ? 'worktree' : undefined;
215
+ const mcpServers = Array.isArray(metadata.mcpServers) && metadata.mcpServers.length > 0
216
+ ? [...metadata.mcpServers]
217
+ : undefined;
218
+ const hooks = typeof metadata.hooks === 'object' &&
219
+ metadata.hooks !== null &&
220
+ !Array.isArray(metadata.hooks) &&
221
+ Object.keys(metadata.hooks).length > 0
222
+ ? { ...metadata.hooks }
223
+ : undefined;
224
+ return {
225
+ ...definition,
226
+ kind: 'agent',
227
+ ...(tools === undefined ? {} : { tools }),
228
+ ...(disallowedTools === undefined ? {} : { disallowedTools }),
229
+ ...(model === undefined ? {} : { model }),
230
+ ...(effort === undefined ? {} : { effort }),
231
+ ...(permissionMode === undefined ? {} : { permissionMode }),
232
+ ...(maxTurns === undefined ? {} : { maxTurns }),
233
+ ...(skills?.length ? { skills } : {}),
234
+ ...(initialPrompt === undefined ? {} : { initialPrompt }),
235
+ ...(memory === undefined ? {} : { memory }),
236
+ ...(background === undefined ? {} : { background }),
237
+ ...(isolation === undefined ? {} : { isolation }),
238
+ ...(mcpServers === undefined ? {} : { mcpServers }),
239
+ ...(hooks === undefined ? {} : { hooks }),
240
+ };
241
+ }
125
242
  export function validateClaudeExtensions(resources) {
126
243
  for (const resource of resources) {
127
244
  const lines = resource.content.split(/\r?\n/);
@@ -216,6 +333,14 @@ function indexed(kind, resources) {
216
333
  }
217
334
  return definitions;
218
335
  }
336
+ function indexedAgents(resources) {
337
+ const definitions = new Map();
338
+ for (const resource of resources) {
339
+ const definition = parseAgentDefinition(resource);
340
+ definitions.set(definition.name, definition);
341
+ }
342
+ return definitions;
343
+ }
219
344
  function substituteArguments(body, argumentsText) {
220
345
  const values = argumentsText.length === 0 ? [] : argumentsText.split(/\s+/);
221
346
  return body.replace(/\$(ARGUMENTS|\d+)/g, (_, key) => key === 'ARGUMENTS' ? argumentsText : (values[Number(key)] ?? ''));
@@ -251,7 +376,7 @@ export class ClaudeExtensionCatalog {
251
376
  : indexed('skill', resources.skills);
252
377
  this.agents = new Map([
253
378
  ['statusline-setup', BUILTIN_STATUSLINE_AGENT],
254
- ...indexed('agent', resources.agents),
379
+ ...indexedAgents(resources.agents),
255
380
  ]);
256
381
  }
257
382
  setMcpPrompts(prompts) {
@@ -1,6 +1,6 @@
1
1
  import type { ClaudeJsonResource } from '../compatibility/claude/shared-resources.js';
2
2
  import type { PermissionBehavior } from '../core/runtime.js';
3
- export type ClaudeHookEventName = 'Setup' | 'SessionStart' | 'UserPromptSubmit' | 'PreToolUse' | 'PermissionRequest' | 'PostToolUse' | 'PostToolUseFailure' | 'Stop' | 'SessionEnd';
3
+ export type ClaudeHookEventName = 'Setup' | 'SessionStart' | 'SubagentStart' | 'UserPromptSubmit' | 'PreToolUse' | 'PermissionRequest' | 'PostToolUse' | 'PostToolUseFailure' | 'Stop' | 'SubagentStop' | 'SessionEnd';
4
4
  export interface ClaudeHookInput extends Record<string, unknown> {
5
5
  session_id: string;
6
6
  transcript_path: string;
@@ -79,7 +79,9 @@ export declare class ClaudeHookRunner {
79
79
  private readonly maxTimeoutMs;
80
80
  private readonly executeCommand;
81
81
  private readonly onEvent;
82
+ private readonly cwd;
82
83
  constructor(options: ClaudeHookRunnerOptions);
84
+ withAdditionalSettings(settings: readonly ClaudeJsonResource[]): ClaudeHookRunner;
83
85
  run(input: ClaudeHookInput, matcherValue?: string, signal?: AbortSignal): Promise<ClaudeHookOutcome>;
84
86
  private runCommand;
85
87
  }
@@ -7,12 +7,14 @@ const KILL_GRACE_MS = 250;
7
7
  const HOOK_EVENTS = [
8
8
  'Setup',
9
9
  'SessionStart',
10
+ 'SubagentStart',
10
11
  'UserPromptSubmit',
11
12
  'PreToolUse',
12
13
  'PermissionRequest',
13
14
  'PostToolUse',
14
15
  'PostToolUseFailure',
15
16
  'Stop',
17
+ 'SubagentStop',
16
18
  'SessionEnd',
17
19
  ];
18
20
  function isRecord(value) {
@@ -137,12 +139,24 @@ export class ClaudeHookRunner {
137
139
  maxTimeoutMs;
138
140
  executeCommand;
139
141
  onEvent;
142
+ cwd;
140
143
  constructor(options) {
141
144
  this.settings = options.settings;
142
145
  this.maxOutputBytes = options.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
143
146
  this.maxTimeoutMs = options.maxTimeoutMs ?? DEFAULT_TIMEOUT_MS;
144
147
  this.executeCommand = options.executeCommand ?? this.runCommand.bind(this);
145
148
  this.onEvent = options.onEvent;
149
+ this.cwd = options.cwd;
150
+ }
151
+ withAdditionalSettings(settings) {
152
+ return new ClaudeHookRunner({
153
+ settings: [...this.settings, ...settings],
154
+ maxOutputBytes: this.maxOutputBytes,
155
+ maxTimeoutMs: this.maxTimeoutMs,
156
+ executeCommand: this.executeCommand,
157
+ ...(this.onEvent ? { onEvent: this.onEvent } : {}),
158
+ cwd: this.cwd,
159
+ });
146
160
  }
147
161
  async run(input, matcherValue, signal) {
148
162
  if (signal?.aborted)