@portel/photon 1.7.0 → 1.8.1

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 (94) hide show
  1. package/README.md +23 -24
  2. package/dist/auto-ui/beam.d.ts.map +1 -1
  3. package/dist/auto-ui/beam.js +117 -42
  4. package/dist/auto-ui/beam.js.map +1 -1
  5. package/dist/auto-ui/design-system/tokens.d.ts +1 -1
  6. package/dist/auto-ui/design-system/tokens.d.ts.map +1 -1
  7. package/dist/auto-ui/design-system/tokens.js +1 -1
  8. package/dist/auto-ui/design-system/tokens.js.map +1 -1
  9. package/dist/auto-ui/frontend/index.html +1 -1
  10. package/dist/auto-ui/rendering/components.d.ts.map +1 -1
  11. package/dist/auto-ui/rendering/components.js +568 -0
  12. package/dist/auto-ui/rendering/components.js.map +1 -1
  13. package/dist/auto-ui/rendering/field-analyzer.d.ts +56 -0
  14. package/dist/auto-ui/rendering/field-analyzer.d.ts.map +1 -1
  15. package/dist/auto-ui/rendering/field-analyzer.js +177 -0
  16. package/dist/auto-ui/rendering/field-analyzer.js.map +1 -1
  17. package/dist/auto-ui/rendering/layout-selector.d.ts +14 -2
  18. package/dist/auto-ui/rendering/layout-selector.d.ts.map +1 -1
  19. package/dist/auto-ui/rendering/layout-selector.js +125 -1
  20. package/dist/auto-ui/rendering/layout-selector.js.map +1 -1
  21. package/dist/auto-ui/streamable-http-transport.d.ts +1 -1
  22. package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
  23. package/dist/auto-ui/streamable-http-transport.js +353 -19
  24. package/dist/auto-ui/streamable-http-transport.js.map +1 -1
  25. package/dist/auto-ui/types.d.ts +7 -1
  26. package/dist/auto-ui/types.d.ts.map +1 -1
  27. package/dist/auto-ui/types.js.map +1 -1
  28. package/dist/beam.bundle.js +22441 -4216
  29. package/dist/beam.bundle.js.map +4 -4
  30. package/dist/cli/commands/info.d.ts.map +1 -1
  31. package/dist/cli/commands/info.js +37 -0
  32. package/dist/cli/commands/info.js.map +1 -1
  33. package/dist/cli/commands/package.d.ts.map +1 -1
  34. package/dist/cli/commands/package.js +16 -0
  35. package/dist/cli/commands/package.js.map +1 -1
  36. package/dist/cli.d.ts.map +1 -1
  37. package/dist/cli.js +628 -14
  38. package/dist/cli.js.map +1 -1
  39. package/dist/context-store.d.ts +79 -0
  40. package/dist/context-store.d.ts.map +1 -0
  41. package/dist/context-store.js +210 -0
  42. package/dist/context-store.js.map +1 -0
  43. package/dist/daemon/client.d.ts +13 -4
  44. package/dist/daemon/client.d.ts.map +1 -1
  45. package/dist/daemon/client.js +138 -77
  46. package/dist/daemon/client.js.map +1 -1
  47. package/dist/daemon/manager.d.ts +0 -25
  48. package/dist/daemon/manager.d.ts.map +1 -1
  49. package/dist/daemon/manager.js +10 -38
  50. package/dist/daemon/manager.js.map +1 -1
  51. package/dist/daemon/protocol.d.ts +7 -2
  52. package/dist/daemon/protocol.d.ts.map +1 -1
  53. package/dist/daemon/protocol.js.map +1 -1
  54. package/dist/daemon/server.js +257 -35
  55. package/dist/daemon/server.js.map +1 -1
  56. package/dist/daemon/session-manager.d.ts +24 -4
  57. package/dist/daemon/session-manager.d.ts.map +1 -1
  58. package/dist/daemon/session-manager.js +62 -12
  59. package/dist/daemon/session-manager.js.map +1 -1
  60. package/dist/index.d.ts +0 -1
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +0 -3
  63. package/dist/index.js.map +1 -1
  64. package/dist/loader.d.ts +3 -20
  65. package/dist/loader.d.ts.map +1 -1
  66. package/dist/loader.js +53 -75
  67. package/dist/loader.js.map +1 -1
  68. package/dist/photon-cli-runner.d.ts.map +1 -1
  69. package/dist/photon-cli-runner.js +258 -218
  70. package/dist/photon-cli-runner.js.map +1 -1
  71. package/dist/photon-doc-extractor.d.ts +2 -0
  72. package/dist/photon-doc-extractor.d.ts.map +1 -1
  73. package/dist/photon-doc-extractor.js +42 -6
  74. package/dist/photon-doc-extractor.js.map +1 -1
  75. package/dist/photons/maker.photon.d.ts.map +1 -1
  76. package/dist/photons/maker.photon.js +3 -1
  77. package/dist/photons/maker.photon.js.map +1 -1
  78. package/dist/photons/maker.photon.ts +3 -1
  79. package/dist/serv/index.d.ts.map +1 -1
  80. package/dist/serv/index.js.map +1 -1
  81. package/dist/server.d.ts +32 -15
  82. package/dist/server.d.ts.map +1 -1
  83. package/dist/server.js +468 -469
  84. package/dist/server.js.map +1 -1
  85. package/dist/shared/security.d.ts.map +1 -1
  86. package/dist/shared/security.js +4 -8
  87. package/dist/shared/security.js.map +1 -1
  88. package/dist/shell-completions.d.ts +21 -0
  89. package/dist/shell-completions.d.ts.map +1 -0
  90. package/dist/shell-completions.js +102 -0
  91. package/dist/shell-completions.js.map +1 -0
  92. package/dist/template-manager.d.ts.map +1 -1
  93. package/dist/template-manager.js.map +1 -1
  94. package/package.json +10 -6
@@ -30,177 +30,52 @@ async function resolvePhotonPathWithBundled(name) {
30
30
  return resolvePhotonPath(name);
31
31
  }
32
32
  import { PhotonDocExtractor } from './photon-doc-extractor.js';
33
- import { isDaemonRunning, startDaemon } from './daemon/manager.js';
33
+ import { isGlobalDaemonRunning, startGlobalDaemon } from './daemon/manager.js';
34
34
  import { sendCommand, pingDaemon } from './daemon/client.js';
35
35
  import { formatOutput as baseFormatOutput, renderNone, formatKey, } from './cli-formatter.js';
36
- import { getErrorMessage } from './shared/error-handler.js';
37
- import { logger } from './shared/logger.js';
36
+ import { getErrorMessage, exitWithError, ExitCode } from './shared/error-handler.js';
38
37
  /**
39
38
  * Extract all public async methods from a photon file
40
39
  */
41
40
  async function extractMethods(filePath) {
42
41
  const source = await fs.readFile(filePath, 'utf-8');
43
42
  const extractor = new SchemaExtractor();
44
- const methods = [];
45
- // Extract methods using the schema extractor
46
- // Also match async generator methods (async *methodName) and static methods
47
- const methodMatches = source.matchAll(/(?:static\s+)?async\s+\*?\s*(\w+)\s*\(([^)]*)\)/g);
48
- for (const match of methodMatches) {
49
- const methodName = match[1];
50
- const methodParams = match[2];
51
- // Skip private methods and lifecycle hooks
52
- if (methodName.startsWith('_') ||
53
- methodName === 'onInitialize' ||
54
- methodName === 'onShutdown') {
55
- continue;
56
- }
57
- // Extract method signature
58
- const methodIndex = match.index;
59
- const precedingContent = source.substring(0, methodIndex);
60
- // Find JSDoc comment
61
- const lastJSDocStart = precedingContent.lastIndexOf('/**');
62
- if (lastJSDocStart === -1)
63
- continue;
64
- const jsdocSection = precedingContent.substring(lastJSDocStart);
65
- const jsdocMatch = jsdocSection.match(/\/\*\*([\s\S]*?)\*\/\s*$/);
66
- if (!jsdocMatch)
67
- continue;
68
- const jsdoc = jsdocMatch[1];
69
- // Extract description
70
- const descMatch = jsdoc.match(/^\s*\*\s*(.+?)(?=\n\s*\*\s*@|\n\s*$)/s);
71
- const description = descMatch
72
- ? descMatch[1]
73
- .split('\n')
74
- .map((line) => line.replace(/^\s*\*\s?/, '').trim())
75
- .join(' ')
76
- .trim()
77
- : undefined;
78
- // Parse TypeScript parameter types and optionality from method signature
79
- const tsParamTypes = new Map();
80
- const tsParamOptional = new Map();
81
- if (methodParams.trim()) {
82
- // Extract: params?: { mute?: boolean } | boolean
83
- const paramTypeMatch = methodParams.match(/(\w+)(\??):\s*(.+)/);
84
- if (paramTypeMatch) {
85
- const tsParamName = paramTypeMatch[1];
86
- const isParamOptional = paramTypeMatch[2] === '?';
87
- const paramType = paramTypeMatch[3].trim();
88
- // Extract base type (handle unions like "{ mute?: boolean } | boolean")
89
- const baseType = extractBaseType(paramType);
90
- // Store type for both the TS param name and any inner property names
91
- tsParamTypes.set(tsParamName, baseType);
92
- tsParamOptional.set(tsParamName, isParamOptional);
93
- // Also extract inner object properties: { mute?: boolean }
94
- const innerProps = extractObjectProperties(paramType);
95
- for (const [propName, propInfo] of innerProps) {
96
- tsParamTypes.set(propName, propInfo.type);
97
- // If the parent param is optional, inner properties are also optional
98
- tsParamOptional.set(propName, isParamOptional || propInfo.optional);
99
- }
100
- }
101
- }
102
- // Extract format hint from @format tag (structural and content formats)
103
- let format;
104
- // Match structural formats (including card, tabs, accordion)
105
- const structuralMatch = jsdoc.match(/@format\s+(primitive|table|tree|list|card|tabs|accordion|none)/i);
106
- if (structuralMatch) {
107
- format = structuralMatch[1].toLowerCase();
108
- }
109
- // Match content formats
110
- else {
111
- const contentMatch = jsdoc.match(/@format\s+(json|markdown|yaml|xml|html)/i);
112
- if (contentMatch) {
113
- format = contentMatch[1].toLowerCase();
114
- }
115
- else {
116
- // Match code format (with optional language)
117
- const codeMatch = jsdoc.match(/@format\s+code(?::(\w+))?/i);
118
- if (codeMatch) {
119
- format = codeMatch[1] ? `code:${codeMatch[1]}` : 'code';
120
- }
121
- }
122
- }
123
- // Extract parameters
43
+ const metadata = extractor.extractAllFromSource(source);
44
+ return metadata.tools.map((tool) => {
124
45
  const params = [];
125
- const paramRegex = /@param\s+(\w+)\s+(.+?)(?=\n\s*\*\s*@|\n\s*\*\/|\n\s*$)/gs;
126
- let paramMatch;
127
- while ((paramMatch = paramRegex.exec(jsdoc)) !== null) {
128
- const paramName = paramMatch[1];
129
- const paramDesc = paramMatch[2].trim();
130
- // Extract custom label from {@label displayName} tag
131
- const labelMatch = paramDesc.match(/\{@label\s+([^}]+)\}/);
132
- const customLabel = labelMatch ? labelMatch[1].trim() : undefined;
133
- // Extract {@example ...} - handle nested braces/brackets for JSON
134
- let example;
135
- const exampleStart = paramDesc.indexOf('{@example ');
136
- if (exampleStart !== -1) {
137
- const contentStart = exampleStart + '{@example '.length;
138
- let braceDepth = 0;
139
- let bracketDepth = 0;
140
- let i = contentStart;
141
- let inString = false;
142
- while (i < paramDesc.length) {
143
- const ch = paramDesc[i];
144
- const prevCh = i > 0 ? paramDesc[i - 1] : '';
145
- if (ch === '"' && prevCh !== '\\') {
146
- inString = !inString;
147
- }
148
- else if (!inString) {
149
- if (ch === '{')
150
- braceDepth++;
151
- else if (ch === '[')
152
- bracketDepth++;
153
- else if (ch === ']')
154
- bracketDepth--;
155
- else if (ch === '}') {
156
- if (braceDepth === 0 && bracketDepth === 0) {
157
- example = paramDesc.substring(contentStart, i).trim();
158
- break;
159
- }
160
- braceDepth--;
161
- }
162
- }
163
- i++;
46
+ const schema = tool.inputSchema;
47
+ if (schema?.properties) {
48
+ for (const [name, prop] of Object.entries(schema.properties)) {
49
+ // Resolve type from anyOf/oneOf union schemas (e.g. number | string)
50
+ let type = prop.type;
51
+ if (!type && (prop.anyOf || prop.oneOf)) {
52
+ const variants = (prop.anyOf || prop.oneOf);
53
+ type = variants
54
+ .map((v) => v.type)
55
+ .filter(Boolean)
56
+ .join(' | ');
164
57
  }
58
+ params.push({
59
+ name,
60
+ type: type || 'any',
61
+ optional: !schema.required?.includes(name),
62
+ description: prop.description,
63
+ ...(prop.title ? { label: prop.title } : {}),
64
+ ...(prop.examples?.[0] !== undefined ? { example: String(prop.examples[0]) } : {}),
65
+ ...(prop.enum ? { enum: prop.enum.map(String) } : {}),
66
+ });
165
67
  }
166
- // Remove {@example ...} tag (handles nested braces/brackets)
167
- let cleanedParamDesc = paramDesc;
168
- if (exampleStart !== -1 && example) {
169
- // Remove the full {@example ...} tag we extracted
170
- const tagEnd = exampleStart + '{@example '.length + example.length + 1; // +1 for closing }
171
- cleanedParamDesc = paramDesc.substring(0, exampleStart) + paramDesc.substring(tagEnd);
172
- }
173
- // Remove remaining JSDoc constraint tags for display
174
- const cleanDesc = cleanedParamDesc
175
- .replace(/\{@\w+[^}]*\}/g, '')
176
- .replace(/\(optional\)/gi, '')
177
- .replace(/\(default:.*?\)/gi, '')
178
- .trim();
179
- // Check optional from TypeScript signature first, fallback to JSDoc
180
- const tsOptional = tsParamOptional.get(paramName);
181
- const jsdocOptional = /\(optional\)/i.test(paramDesc) || /\(default:/i.test(paramDesc);
182
- const optional = tsOptional !== undefined ? tsOptional : jsdocOptional;
183
- params.push({
184
- name: paramName,
185
- type: tsParamTypes.get(paramName) || 'any',
186
- optional,
187
- description: cleanDesc,
188
- ...(customLabel ? { label: customLabel } : {}),
189
- ...(example ? { example } : {}),
190
- });
191
68
  }
192
- // Extract button label from @returns {@label ...} tag
193
- const returnsMatch = jsdoc.match(/@returns?\s+.*?\{@label\s+([^}]+)\}/i);
194
- const buttonLabel = returnsMatch ? returnsMatch[1].trim() : undefined;
195
- methods.push({
196
- name: methodName,
69
+ return {
70
+ name: tool.name,
197
71
  params,
198
- description,
199
- ...(format ? { format } : {}),
200
- ...(buttonLabel ? { buttonLabel } : {}),
201
- });
202
- }
203
- return methods;
72
+ description: tool.description !== 'No description' ? tool.description : undefined,
73
+ ...(tool.outputFormat ? { format: tool.outputFormat } : {}),
74
+ ...(tool.buttonLabel ? { buttonLabel: tool.buttonLabel } : {}),
75
+ ...(tool.scheduled ? { scheduled: tool.scheduled } : {}),
76
+ ...(tool.webhook !== undefined ? { webhook: true } : {}),
77
+ };
78
+ });
204
79
  }
205
80
  /**
206
81
  * Extract base type from TypeScript type annotation
@@ -252,6 +127,45 @@ function extractObjectProperties(typeStr) {
252
127
  */
253
128
  function formatOutput(result, formatHint) {
254
129
  let hint = formatHint;
130
+ // Handle _photonType structured data (e.g., table, collection)
131
+ if (result && typeof result === 'object' && result._photonType) {
132
+ // If the object has toJSON(), call it to get the plain data (e.g., Table instances have private fields)
133
+ const data = typeof result.toJSON === 'function' ? result.toJSON() : result;
134
+ const photonType = data._photonType;
135
+ if (photonType === 'table' && data.rows && Array.isArray(data.rows)) {
136
+ // Convert structured table to array of objects for renderTable
137
+ const columns = data.columns || [];
138
+ const rows = data.rows.map((row) => {
139
+ if (Array.isArray(row)) {
140
+ // Row is an array of values, map to column names
141
+ const obj = {};
142
+ columns.forEach((col, i) => {
143
+ const key = typeof col === 'string' ? col : col.label || col.field || `col${i}`;
144
+ obj[key] = row[i] ?? '';
145
+ });
146
+ return obj;
147
+ }
148
+ // Row is already an object, extract display values using column fields
149
+ if (columns.length > 0) {
150
+ const obj = {};
151
+ for (const col of columns) {
152
+ const field = typeof col === 'string' ? col : col.field || col.key;
153
+ const label = typeof col === 'string' ? col : col.label || col.field || col.key;
154
+ if (field) {
155
+ obj[label] = row[field] ?? '';
156
+ }
157
+ }
158
+ return obj;
159
+ }
160
+ return row;
161
+ });
162
+ baseFormatOutput(rows, 'table');
163
+ return true;
164
+ }
165
+ // For other _photonType values, strip the marker and render normally
166
+ const { _photonType, ...rest } = data;
167
+ return formatOutput(rest, formatHint);
168
+ }
255
169
  // Handle error responses
256
170
  if (result && typeof result === 'object' && result.success === false) {
257
171
  const errorMsg = result.error || result.message || 'Unknown error';
@@ -371,7 +285,18 @@ function parseCliArgs(args, params) {
371
285
  let positionalIndex = 0;
372
286
  for (let i = 0; i < args.length; i++) {
373
287
  const arg = args[i];
374
- if (arg.startsWith('--')) {
288
+ if (arg.startsWith('--no-') && !arg.includes('=')) {
289
+ // --no-<param> negation syntax for boolean params (e.g., --no-enabled → enabled=false)
290
+ const key = arg.substring(5);
291
+ if (paramTypes.has(key)) {
292
+ result[key] = false;
293
+ }
294
+ else {
295
+ // Unknown param, store as-is (will be caught by validation later)
296
+ result[key] = false;
297
+ }
298
+ }
299
+ else if (arg.startsWith('--')) {
375
300
  // Named argument: --key value or --key=value
376
301
  const eqIndex = arg.indexOf('=');
377
302
  if (eqIndex !== -1) {
@@ -384,13 +309,17 @@ function parseCliArgs(args, params) {
384
309
  else {
385
310
  // --key value format (next arg is the value)
386
311
  const key = arg.substring(2);
387
- i++;
388
- if (i < args.length) {
389
- const expectedType = paramTypes.get(key) || 'any';
312
+ const expectedType = paramTypes.get(key) || 'any';
313
+ // For boolean params, treat bare --flag as true (no value consumed)
314
+ if (expectedType === 'boolean' && (i + 1 >= args.length || args[i + 1].startsWith('--'))) {
315
+ result[key] = true;
316
+ }
317
+ else if (i + 1 < args.length) {
318
+ i++;
390
319
  result[key] = coerceValue(args[i], expectedType);
391
320
  }
392
321
  else {
393
- // Boolean flag
322
+ // No value and not boolean - treat as boolean flag
394
323
  result[key] = true;
395
324
  }
396
325
  }
@@ -412,7 +341,7 @@ function parseCliArgs(args, params) {
412
341
  function coerceValue(value, expectedType) {
413
342
  // Preserve strings starting with + or - for relative adjustments
414
343
  // Must check BEFORE JSON.parse because JSON.parse("-3") returns -3 (number)
415
- if (expectedType === 'number' && (value.startsWith('+') || value.startsWith('-'))) {
344
+ if (expectedType.includes('number') && (value.startsWith('+') || value.startsWith('-'))) {
416
345
  return value;
417
346
  }
418
347
  // Try to parse as JSON first (handles objects, arrays, true/false)
@@ -987,9 +916,57 @@ function formatLabel(name) {
987
916
  /**
988
917
  * Print help for a specific method
989
918
  */
919
+ function printParamHelp(param) {
920
+ const displayLabel = param.label || formatLabel(param.name);
921
+ // Build type hint suffix
922
+ let typeHint = '';
923
+ if (param.enum && param.enum.length > 0) {
924
+ typeHint = ` [values: ${param.enum.join(', ')}]`;
925
+ }
926
+ else if (param.type === 'boolean') {
927
+ typeHint = ' [--no-' + param.name + ' to disable]';
928
+ }
929
+ else if (param.type === 'array') {
930
+ typeHint = ` (JSON array, e.g., '["a","b"]')`;
931
+ }
932
+ else if (param.type === 'object') {
933
+ typeHint = ` (JSON object, e.g., '{"key":"value"}')`;
934
+ }
935
+ console.log(` --${param.name} (${displayLabel})${typeHint}`);
936
+ if (param.description) {
937
+ console.log(` ${param.description}`);
938
+ }
939
+ // Show example for complex types (JSON)
940
+ if (param.example) {
941
+ console.log(` Example: ${param.example}`);
942
+ }
943
+ }
990
944
  function printMethodHelp(photonName, method) {
945
+ // Truncate description at sentence boundary if too long, or clean up mid-sentence truncation
946
+ let description = method.description || 'No description';
947
+ if (description.length > 200) {
948
+ const sentenceEnd = description.substring(0, 200).lastIndexOf('.');
949
+ if (sentenceEnd > 80) {
950
+ description = description.substring(0, sentenceEnd + 1);
951
+ }
952
+ else {
953
+ description = description.substring(0, 197) + '...';
954
+ }
955
+ }
956
+ else if (description.length > 0 && !/[.!?]$/.test(description.trim())) {
957
+ // Description appears truncated mid-sentence (no terminal punctuation)
958
+ // Truncate at the last complete sentence if possible
959
+ const lastSentence = description.lastIndexOf('.');
960
+ if (lastSentence > 40) {
961
+ description = description.substring(0, lastSentence + 1);
962
+ }
963
+ else {
964
+ // No good sentence boundary — add ellipsis to signal truncation
965
+ description = description.trim() + '...';
966
+ }
967
+ }
991
968
  console.log(`\nNAME:`);
992
- console.log(` ${method.name} - ${method.description || 'No description'}\n`);
969
+ console.log(` ${method.name} - ${description}\n`);
993
970
  console.log(`USAGE:`);
994
971
  const requiredParams = method.params.filter((p) => !p.optional);
995
972
  const optionalParams = method.params.filter((p) => p.optional);
@@ -1005,32 +982,14 @@ function printMethodHelp(photonName, method) {
1005
982
  if (requiredParams.length > 0) {
1006
983
  console.log(`REQUIRED:`);
1007
984
  for (const param of requiredParams) {
1008
- // Show custom label or formatted name
1009
- const displayLabel = param.label || formatLabel(param.name);
1010
- console.log(` --${param.name} (${displayLabel})`);
1011
- if (param.description) {
1012
- console.log(` ${param.description}`);
1013
- }
1014
- // Show example for complex types (JSON)
1015
- if (param.example) {
1016
- console.log(` Example: ${param.example}`);
1017
- }
985
+ printParamHelp(param);
1018
986
  }
1019
987
  console.log('');
1020
988
  }
1021
989
  if (optionalParams.length > 0) {
1022
990
  console.log(`OPTIONS:`);
1023
991
  for (const param of optionalParams) {
1024
- // Show custom label or formatted name
1025
- const displayLabel = param.label || formatLabel(param.name);
1026
- console.log(` --${param.name} (${displayLabel})`);
1027
- if (param.description) {
1028
- console.log(` ${param.description}`);
1029
- }
1030
- // Show example for complex types (JSON)
1031
- if (param.example) {
1032
- console.log(` Example: ${param.example}`);
1033
- }
992
+ printParamHelp(param);
1034
993
  }
1035
994
  console.log('');
1036
995
  }
@@ -1054,12 +1013,28 @@ export async function listMethods(photonName) {
1054
1013
  try {
1055
1014
  const resolvedPath = await resolvePhotonPathWithBundled(photonName);
1056
1015
  if (!resolvedPath) {
1057
- logger.error(`Photon '${photonName}' not found`);
1058
- console.error(`\nMake sure the photon is installed in ~/.photon/`);
1059
- console.error(`If '${photonName}' is a command, run 'photon --help' for available commands.`);
1060
- process.exit(1);
1016
+ exitWithError(`Photon '${photonName}' not found`, {
1017
+ exitCode: ExitCode.NOT_FOUND,
1018
+ searchedIn: '~/.photon/',
1019
+ suggestion: `Install it with: photon add ${photonName}\nIf '${photonName}' is a command, run 'photon --help' for available commands`,
1020
+ });
1061
1021
  }
1062
- const methods = await extractMethods(resolvedPath);
1022
+ const allMethods = await extractMethods(resolvedPath);
1023
+ // Filter out internal methods: scheduled*, handle*, reportError
1024
+ const methods = allMethods.filter((m) => {
1025
+ if (m.scheduled)
1026
+ return false;
1027
+ if (m.webhook)
1028
+ return false;
1029
+ if (m.name.startsWith('scheduled'))
1030
+ return false;
1031
+ if (m.name.startsWith('handle'))
1032
+ return false;
1033
+ if (m.name === 'reportError')
1034
+ return false;
1035
+ return true;
1036
+ });
1037
+ const hiddenCount = allMethods.length - methods.length;
1063
1038
  // Print usage
1064
1039
  console.log(`\nUSAGE:`);
1065
1040
  console.log(` photon cli ${photonName} <command> [options]\n`);
@@ -1067,11 +1042,32 @@ export async function listMethods(photonName) {
1067
1042
  console.log(`COMMANDS:`);
1068
1043
  // Find longest method name for alignment
1069
1044
  const maxLength = Math.max(...methods.map((m) => m.name.length));
1045
+ const maxDescLength = 60;
1070
1046
  for (const method of methods) {
1071
1047
  const padding = ' '.repeat(maxLength - method.name.length + 4);
1072
- const description = method.description || 'No description';
1048
+ let description = method.description || 'No description';
1049
+ // Truncate long descriptions in listing, keeping first sentence
1050
+ if (description.length > maxDescLength) {
1051
+ const sentenceEnd = description.substring(0, maxDescLength).lastIndexOf('.');
1052
+ if (sentenceEnd > 20) {
1053
+ description = description.substring(0, sentenceEnd + 1);
1054
+ }
1055
+ else {
1056
+ description = description.substring(0, maxDescLength - 3) + '...';
1057
+ }
1058
+ }
1059
+ // Show param signature for methods with no description
1060
+ if (!method.description && method.params.length > 0) {
1061
+ const sig = method.params
1062
+ .map((p) => (p.optional ? `${p.name}?` : p.name) + `: ${p.type}`)
1063
+ .join(', ');
1064
+ description = `(${sig})`;
1065
+ }
1073
1066
  console.log(` ${method.name}${padding}${description}`);
1074
1067
  }
1068
+ if (hiddenCount > 0) {
1069
+ console.log(`\n (${hiddenCount} internal/scheduled methods hidden)`);
1070
+ }
1075
1071
  // Print footer
1076
1072
  console.log(`\nFor detailed parameter information, run:`);
1077
1073
  console.log(` photon cli ${photonName} <command> --help\n`);
@@ -1094,8 +1090,9 @@ export async function listMethods(photonName) {
1094
1090
  }
1095
1091
  }
1096
1092
  catch (error) {
1097
- logger.error(`Error: ${getErrorMessage(error)}`);
1098
- process.exit(1);
1093
+ exitWithError(`Cannot list methods for ${photonName}: ${getErrorMessage(error)}`, {
1094
+ suggestion: `Verify the photon file is valid: ~/.photon/${photonName}.photon.ts`,
1095
+ });
1099
1096
  }
1100
1097
  }
1101
1098
  /**
@@ -1122,10 +1119,11 @@ export async function runMethod(photonName, methodName, args) {
1122
1119
  // Resolve photon path
1123
1120
  const resolvedPath = await resolvePhotonPathWithBundled(photonName);
1124
1121
  if (!resolvedPath) {
1125
- logger.error(`Photon '${photonName}' not found`);
1126
- console.error(`\nMake sure the photon is installed in ~/.photon/`);
1127
- console.error(`If '${photonName}' is a command, run 'photon --help' for available commands.`);
1128
- process.exit(1);
1122
+ exitWithError(`Photon '${photonName}' not found`, {
1123
+ exitCode: ExitCode.NOT_FOUND,
1124
+ searchedIn: '~/.photon/',
1125
+ suggestion: `Install it with: photon add ${photonName}\nIf '${photonName}' is a command, run 'photon --help' for available commands`,
1126
+ });
1129
1127
  }
1130
1128
  // Extract MCP name from filename
1131
1129
  const mcpName = path.basename(resolvedPath).replace(/\.photon\.(ts|js)$/, '');
@@ -1138,10 +1136,11 @@ export async function runMethod(photonName, methodName, args) {
1138
1136
  }
1139
1137
  const method = methods.find((m) => m.name === methodName);
1140
1138
  if (!method) {
1141
- logger.error(`Method '${methodName}' not found in ${photonName}`);
1142
- console.error(`\nAvailable methods: ${methods.map((m) => m.name).join(', ')}`);
1143
- console.error(`\nRun 'photon cli ${photonName}' to see all methods`);
1144
- process.exit(1);
1139
+ const available = methods.map((m) => m.name).join(', ');
1140
+ exitWithError(`Method '${methodName}' not found in ${photonName}`, {
1141
+ exitCode: ExitCode.NOT_FOUND,
1142
+ suggestion: `Available methods: ${available}\nRun 'photon cli ${photonName}' to see details`,
1143
+ });
1145
1144
  }
1146
1145
  // Check for --help flag in args (e.g., photon cli test-cli-calc add --help)
1147
1146
  if (args.includes('--help') || args.includes('-h')) {
@@ -1168,11 +1167,47 @@ export async function runMethod(photonName, methodName, args) {
1168
1167
  }
1169
1168
  }
1170
1169
  if (missing.length > 0) {
1171
- logger.error(`Missing required parameters: ${missing.join(', ')}`);
1172
- console.error(`\nUsage: photon cli ${photonName} ${methodName} ${method.params
1170
+ const usage = method.params
1173
1171
  .map((p) => (p.optional ? `[--${p.name}]` : `--${p.name} <value>`))
1174
- .join(' ')}`);
1175
- process.exit(1);
1172
+ .join(' ');
1173
+ const details = missing
1174
+ .map((name) => {
1175
+ const p = method.params.find((mp) => mp.name === name);
1176
+ return ` --${name} (${p?.type || 'string'})${p?.description ? ': ' + p.description : ''}`;
1177
+ })
1178
+ .join('\n');
1179
+ exitWithError(`Missing required parameters: ${missing.join(', ')}`, {
1180
+ exitCode: ExitCode.INVALID_ARGUMENT,
1181
+ suggestion: `Usage: photon cli ${photonName} ${methodName} ${usage}\n\nRequired:\n${details}`,
1182
+ });
1183
+ }
1184
+ // Validate parameter types and enum values
1185
+ const validationErrors = [];
1186
+ for (const param of method.params) {
1187
+ if (!(param.name in parsedArgs))
1188
+ continue;
1189
+ const value = parsedArgs[param.name];
1190
+ // Validate number types
1191
+ if (param.type === 'number' && typeof value !== 'number') {
1192
+ // coerceToType returns the original string if NaN, so check for non-number
1193
+ if (typeof value === 'string' && !(value.startsWith('+') || value.startsWith('-'))) {
1194
+ validationErrors.push(` --${param.name}: expected a number, got '${value}'`);
1195
+ }
1196
+ }
1197
+ // Validate enum values
1198
+ if (param.enum && param.enum.length > 0) {
1199
+ const strValue = String(value).toLowerCase();
1200
+ const validValues = param.enum.map((v) => v.toLowerCase());
1201
+ if (!validValues.includes(strValue)) {
1202
+ validationErrors.push(` --${param.name}: invalid value '${value}' (must be one of: ${param.enum.join(', ')})`);
1203
+ }
1204
+ }
1205
+ }
1206
+ if (validationErrors.length > 0) {
1207
+ exitWithError(`Invalid parameter values`, {
1208
+ exitCode: ExitCode.INVALID_ARGUMENT,
1209
+ suggestion: validationErrors.join('\n'),
1210
+ });
1176
1211
  }
1177
1212
  // Check if photon is stateful
1178
1213
  const extractor = new PhotonDocExtractor(resolvedPath);
@@ -1181,8 +1216,8 @@ export async function runMethod(photonName, methodName, args) {
1181
1216
  if (metadata.stateful) {
1182
1217
  // STATEFUL PATH: Use daemon
1183
1218
  // Check if daemon is running
1184
- if (!isDaemonRunning(photonName)) {
1185
- await startDaemon(photonName, resolvedPath, true); // quiet mode
1219
+ if (!isGlobalDaemonRunning()) {
1220
+ await startGlobalDaemon(true);
1186
1221
  // Wait for daemon to be ready
1187
1222
  let ready = false;
1188
1223
  for (let i = 0; i < 10; i++) {
@@ -1193,12 +1228,19 @@ export async function runMethod(photonName, methodName, args) {
1193
1228
  }
1194
1229
  }
1195
1230
  if (!ready) {
1196
- logger.error(`Failed to start daemon for ${photonName}`);
1197
- process.exit(1);
1231
+ exitWithError(`Failed to start daemon for ${photonName}`, {
1232
+ suggestion: `Check logs: cat ~/.photon/daemons/${photonName}/daemon.log\nOr try: photon daemon restart ${photonName}`,
1233
+ });
1198
1234
  }
1199
1235
  }
1200
- // Send command to daemon
1201
- result = await sendCommand(photonName, methodName, parsedArgs);
1236
+ // Read session-scoped instance set by `photon use` (scoped to this terminal)
1237
+ const { CLISessionStore } = await import('./context-store.js');
1238
+ const sessionInstance = new CLISessionStore().getCurrentInstance(photonName);
1239
+ const sessionId = `cli-${photonName}`;
1240
+ const sendOpts = { photonPath: resolvedPath, sessionId };
1241
+ await sendCommand(photonName, '_use', { name: sessionInstance }, sendOpts);
1242
+ // Send the actual command
1243
+ result = await sendCommand(photonName, methodName, parsedArgs, sendOpts);
1202
1244
  }
1203
1245
  else {
1204
1246
  // STATELESS PATH: Direct execution
@@ -1244,15 +1286,13 @@ export async function runMethod(photonName, methodName, args) {
1244
1286
  catch (error) {
1245
1287
  // Check for custom user-facing message from photon
1246
1288
  // Photons can throw: throw Object.assign(new Error('internal'), { userMessage: 'friendly msg', hint: 'try this' })
1247
- const userMessage = (error && typeof error === 'object' && 'userMessage' in error && error.userMessage) ||
1289
+ const userMessage = (error && typeof error === 'object' && 'userMessage' in error && String(error.userMessage)) ||
1248
1290
  getErrorMessage(error) ||
1249
1291
  'Unknown error occurred';
1250
- const hint = error && typeof error === 'object' && 'hint' in error ? error.hint : undefined;
1251
- logger.error(`${userMessage}`);
1252
- if (hint) {
1253
- console.error(`💡 ${hint}`);
1254
- }
1255
- process.exit(1);
1292
+ const hint = error && typeof error === 'object' && 'hint' in error ? String(error.hint) : undefined;
1293
+ exitWithError(String(userMessage), {
1294
+ suggestion: hint ? String(hint) : undefined,
1295
+ });
1256
1296
  }
1257
1297
  }
1258
1298
  /**