dr-claw 1.1.2 → 1.1.4

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 (113) hide show
  1. package/README.md +286 -177
  2. package/README.zh-CN.md +286 -174
  3. package/dist/assets/{_basePickBy-C0n1uo4u.js → _basePickBy-7CEpiVqc.js} +1 -1
  4. package/dist/assets/{_baseUniq-BRNmhFkc.js → _baseUniq-yh8GoKah.js} +1 -1
  5. package/dist/assets/{arc-DOMBmC_A.js → arc-xxYot-So.js} +1 -1
  6. package/dist/assets/{architectureDiagram-VXUJARFQ-CtMWglbD.js → architectureDiagram-VXUJARFQ-Bb1NqOFB.js} +1 -1
  7. package/dist/assets/{blockDiagram-VD42YOAC-DK_gTkJy.js → blockDiagram-VD42YOAC-D8xMHk8C.js} +1 -1
  8. package/dist/assets/{c4Diagram-YG6GDRKO-DrDAvsn7.js → c4Diagram-YG6GDRKO-69UiBMVa.js} +1 -1
  9. package/dist/assets/channel-BJdHHkhR.js +1 -0
  10. package/dist/assets/{chunk-4BX2VUAB-CGbhdZcN.js → chunk-4BX2VUAB-DXrPhE6K.js} +1 -1
  11. package/dist/assets/{chunk-55IACEB6-sGeFSxen.js → chunk-55IACEB6-B3sw4AGX.js} +1 -1
  12. package/dist/assets/{chunk-B4BG7PRW-RxgHAU_P.js → chunk-B4BG7PRW-Dd4n17kR.js} +1 -1
  13. package/dist/assets/{chunk-DI55MBZ5-CvHUXZrx.js → chunk-DI55MBZ5-DuTb9gb8.js} +1 -1
  14. package/dist/assets/{chunk-FMBD7UC4-Sn7cXVNU.js → chunk-FMBD7UC4-CGErQ-ud.js} +1 -1
  15. package/dist/assets/{chunk-QN33PNHL-bDKOHppN.js → chunk-QN33PNHL-Bk5fuYbR.js} +1 -1
  16. package/dist/assets/{chunk-QZHKN3VN-B9kN5LGm.js → chunk-QZHKN3VN-2UdZTbge.js} +1 -1
  17. package/dist/assets/{chunk-TZMSLE5B-CCBe6len.js → chunk-TZMSLE5B-Bl-UYKEK.js} +1 -1
  18. package/dist/assets/{classDiagram-2ON5EDUG-DQoMNYlY.js → classDiagram-2ON5EDUG-EaoBDWqJ.js} +1 -1
  19. package/dist/assets/{classDiagram-v2-WZHVMYZB-DQoMNYlY.js → classDiagram-v2-WZHVMYZB-EaoBDWqJ.js} +1 -1
  20. package/dist/assets/clone-CwXTbwxl.js +1 -0
  21. package/dist/assets/{cose-bilkent-S5V4N54A-r6sLiHBW.js → cose-bilkent-S5V4N54A-qXNKLX2R.js} +1 -1
  22. package/dist/assets/{dagre-6UL2VRFP-AbsaR1Oa.js → dagre-6UL2VRFP-DmHvDWep.js} +1 -1
  23. package/dist/assets/{diagram-PSM6KHXK-DjCvdUyy.js → diagram-PSM6KHXK-C_Rj4DX3.js} +1 -1
  24. package/dist/assets/{diagram-QEK2KX5R-CqiocXkH.js → diagram-QEK2KX5R-BVr3mPXW.js} +1 -1
  25. package/dist/assets/{diagram-S2PKOQOG-D5ALeoII.js → diagram-S2PKOQOG-DVx6i0Qe.js} +1 -1
  26. package/dist/assets/{erDiagram-Q2GNP2WA-C1AeOsiz.js → erDiagram-Q2GNP2WA-BJQyv9_t.js} +1 -1
  27. package/dist/assets/{flowDiagram-NV44I4VS-C98ve_DO.js → flowDiagram-NV44I4VS-DCR8sPAE.js} +1 -1
  28. package/dist/assets/{ganttDiagram-JELNMOA3-3zdLvcxD.js → ganttDiagram-JELNMOA3-BcXFAthK.js} +1 -1
  29. package/dist/assets/{gitGraphDiagram-V2S2FVAM-nDuDMsHh.js → gitGraphDiagram-V2S2FVAM-CnS56w1x.js} +1 -1
  30. package/dist/assets/{graph-Blm-emkR.js → graph-DGtZXuDY.js} +1 -1
  31. package/dist/assets/index-C-sL-ag0.css +1 -0
  32. package/dist/assets/index-VdqxIFPL.js +670 -0
  33. package/dist/assets/{infoDiagram-HS3SLOUP-CmzE3q0R.js → infoDiagram-HS3SLOUP-DtI272ZD.js} +1 -1
  34. package/dist/assets/{journeyDiagram-XKPGCS4Q-CYaW7LrP.js → journeyDiagram-XKPGCS4Q-DHp_Kica.js} +1 -1
  35. package/dist/assets/{kanban-definition-3W4ZIXB7-mkmW7p0j.js → kanban-definition-3W4ZIXB7-CjgHMNku.js} +1 -1
  36. package/dist/assets/{layout-Ct2kFIId.js → layout-Cnc-DCSb.js} +1 -1
  37. package/dist/assets/{linear-CKG9EwIJ.js → linear-BtRgmV19.js} +1 -1
  38. package/dist/assets/{mermaid.core-CaKcUnnd.js → mermaid.core-B20uzv6N.js} +5 -5
  39. package/dist/assets/{mindmap-definition-VGOIOE7T-Bct-pYin.js → mindmap-definition-VGOIOE7T-bbcLXkWp.js} +1 -1
  40. package/dist/assets/{pieDiagram-ADFJNKIX-C8wFD4Jr.js → pieDiagram-ADFJNKIX-B4fEzWKO.js} +1 -1
  41. package/dist/assets/{quadrantDiagram-AYHSOK5B-CuiE_f46.js → quadrantDiagram-AYHSOK5B-B5G0SMbK.js} +1 -1
  42. package/dist/assets/{requirementDiagram-UZGBJVZJ-C4v1CqU8.js → requirementDiagram-UZGBJVZJ-DsqtOFgj.js} +1 -1
  43. package/dist/assets/{sankeyDiagram-TZEHDZUN-keAqqs-Y.js → sankeyDiagram-TZEHDZUN-CiGemFRE.js} +1 -1
  44. package/dist/assets/{sequenceDiagram-WL72ISMW-ClNbvKTB.js → sequenceDiagram-WL72ISMW-hEMvYbe9.js} +1 -1
  45. package/dist/assets/{stateDiagram-FKZM4ZOC-D8We-2vq.js → stateDiagram-FKZM4ZOC-Cn8VB7b1.js} +1 -1
  46. package/dist/assets/stateDiagram-v2-4FDKWEC3-DaH0PZOq.js +1 -0
  47. package/dist/assets/{timeline-definition-IT6M3QCI-DsvEMT5T.js → timeline-definition-IT6M3QCI-DChT1GUr.js} +1 -1
  48. package/dist/assets/{treemap-GDKQZRPO-TJrzN7Bj.js → treemap-GDKQZRPO-Dflv6a8z.js} +1 -1
  49. package/dist/assets/{xychartDiagram-PRI3JC2R-BdATmY5F.js → xychartDiagram-PRI3JC2R-Dr4urXR8.js} +1 -1
  50. package/dist/icons/nanoclaude.png +0 -0
  51. package/dist/index.html +2 -2
  52. package/dist/screenshots/screenshot_v1_2.png +0 -0
  53. package/package.json +61 -4
  54. package/public/icons/nanoclaude.png +0 -0
  55. package/public/screenshots/screenshot_v1_2.png +0 -0
  56. package/scripts/build-electron-icons.mjs +40 -0
  57. package/scripts/check-deps.js +28 -0
  58. package/scripts/native-runtime.mjs +128 -0
  59. package/server/__tests__/gemini-api.test.mjs +699 -0
  60. package/server/__tests__/gemini-session-index.test.mjs +95 -0
  61. package/server/__tests__/project-sync-dedup.test.mjs +173 -0
  62. package/server/__tests__/session-delete.test.mjs +152 -0
  63. package/server/claude-sdk.js +175 -14
  64. package/server/cli-chat.js +28 -17
  65. package/server/cli.js +48 -11
  66. package/server/data/news-log-arxiv.json +1 -0
  67. package/server/data/news-log-huggingface.json +1 -0
  68. package/server/data/news-results-arxiv.json +460 -0
  69. package/server/data/news-results-huggingface.json +817 -0
  70. package/server/data/research_interests_arxiv.json +50 -0
  71. package/server/data/research_interests_huggingface.json +4 -0
  72. package/server/database/db.js +101 -5
  73. package/server/database/init.sql +17 -1
  74. package/server/gemini-api.js +1838 -0
  75. package/server/gemini-cli.js +13 -2
  76. package/server/index.js +94 -16
  77. package/server/load-env.js +13 -1
  78. package/server/local-gpu.js +166 -24
  79. package/server/mcp-compute.js +223 -0
  80. package/server/middleware/auth.js +25 -6
  81. package/server/nano-claude-code.js +340 -0
  82. package/server/nanoSessionPaths.js +41 -0
  83. package/server/openai-codex.js +6 -2
  84. package/server/openrouter.js +37 -22
  85. package/server/projects.js +516 -110
  86. package/server/routes/agent.js +32 -7
  87. package/server/routes/auto-research.js +1 -1
  88. package/server/routes/claude-btw.js +55 -0
  89. package/server/routes/cli-auth.js +422 -58
  90. package/server/routes/commands.js +25 -1
  91. package/server/routes/community-tools.js +398 -0
  92. package/server/routes/compute.js +155 -0
  93. package/server/routes/git.js +9 -11
  94. package/server/routes/memory.js +143 -0
  95. package/server/routes/news.js +3 -0
  96. package/server/routes/skills.js +35 -0
  97. package/server/routes/user.js +3 -5
  98. package/server/utils/__tests__/safePath.test.js +93 -0
  99. package/server/utils/cliResolution.js +13 -1
  100. package/server/utils/codexSessionMessages.js +285 -0
  101. package/server/utils/geminiOAuth.js +333 -0
  102. package/server/utils/memoryPrompt.js +78 -0
  103. package/server/utils/safePath.js +51 -0
  104. package/server/utils/sessionIndex.js +2 -0
  105. package/server/utils/skillExpander.js +142 -0
  106. package/server/utils/spawnAsync.js +78 -0
  107. package/shared/modelConstants.js +19 -1
  108. package/dist/assets/channel-BFOE7OyC.js +0 -1
  109. package/dist/assets/clone-BulQSblH.js +0 -1
  110. package/dist/assets/index-B8Yutsnm.css +0 -1
  111. package/dist/assets/index-Cf6gyhc3.js +0 -654
  112. package/dist/assets/stateDiagram-v2-4FDKWEC3-BUURfjrE.js +0 -1
  113. package/scripts/install-python-tools.js +0 -64
@@ -0,0 +1,223 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Compute Tools MCP Server
5
+ *
6
+ * Exposes compute node operations (run, sync, slurm) as MCP tools
7
+ * that the AI agent can use during a chat session.
8
+ *
9
+ * Launched as a child process by the Claude Agent SDK via mcpServers config.
10
+ */
11
+
12
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
13
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
14
+ import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
15
+ import { ComputeNode, getActiveNode } from './compute-node.js';
16
+
17
+ const server = new Server(
18
+ { name: 'compute-tools', version: '1.0.0' },
19
+ { capabilities: { tools: {} } },
20
+ );
21
+
22
+ // ─── Tool definitions ───
23
+
24
+ const TOOLS = [
25
+ {
26
+ name: 'compute_info',
27
+ description:
28
+ 'Get information about the active remote compute node, including hostname, type, and GPU status. Use this first to check what compute resources are available.',
29
+ inputSchema: {
30
+ type: 'object',
31
+ properties: {},
32
+ },
33
+ },
34
+ {
35
+ name: 'compute_run',
36
+ description:
37
+ 'Run a shell command on the active remote compute node via SSH. Use this for GPU tasks, training scripts, or any command that should execute on the remote server instead of locally.',
38
+ inputSchema: {
39
+ type: 'object',
40
+ properties: {
41
+ command: {
42
+ type: 'string',
43
+ description: 'The shell command to execute on the remote node',
44
+ },
45
+ cwd: {
46
+ type: 'string',
47
+ description:
48
+ 'Local working directory. If provided and skipSync is false, code will be synced to the remote node before execution.',
49
+ },
50
+ skipSync: {
51
+ type: 'boolean',
52
+ description:
53
+ 'If true, skip syncing code before running. Default: true. Set to false if you need to sync local code first.',
54
+ default: true,
55
+ },
56
+ },
57
+ required: ['command'],
58
+ },
59
+ },
60
+ {
61
+ name: 'compute_sync',
62
+ description:
63
+ 'Sync project code between the local machine and the active remote compute node. Use "up" to push local code to remote, "down" to pull results back.',
64
+ inputSchema: {
65
+ type: 'object',
66
+ properties: {
67
+ direction: {
68
+ type: 'string',
69
+ enum: ['up', 'down'],
70
+ description: '"up" = push local→remote, "down" = pull remote→local',
71
+ },
72
+ cwd: {
73
+ type: 'string',
74
+ description: 'Local project directory to sync',
75
+ },
76
+ files: {
77
+ type: 'array',
78
+ items: { type: 'string' },
79
+ description:
80
+ 'For "down" direction: specific file patterns to pull (e.g., ["logs/", "checkpoints/"]). Defaults to logs/, checkpoints/, results/.',
81
+ },
82
+ },
83
+ required: ['direction', 'cwd'],
84
+ },
85
+ },
86
+ {
87
+ name: 'compute_slurm_submit',
88
+ description:
89
+ 'Submit a Slurm batch job on the active compute node. Provide a full sbatch script including #SBATCH directives. Only works on Slurm HPC nodes.',
90
+ inputSchema: {
91
+ type: 'object',
92
+ properties: {
93
+ script: {
94
+ type: 'string',
95
+ description:
96
+ 'Full sbatch script content including #!/bin/bash and #SBATCH directives',
97
+ },
98
+ },
99
+ required: ['script'],
100
+ },
101
+ },
102
+ {
103
+ name: 'compute_slurm_queue',
104
+ description:
105
+ 'List current Slurm jobs on the active compute node. Shows job ID, name, state, and elapsed time. Only works on Slurm HPC nodes.',
106
+ inputSchema: {
107
+ type: 'object',
108
+ properties: {},
109
+ },
110
+ },
111
+ ];
112
+
113
+ // ─── Tool handlers ───
114
+
115
+ async function requireActiveNode() {
116
+ const node = await getActiveNode();
117
+ if (!node) {
118
+ throw new Error(
119
+ 'No active compute node configured. Please set one in the Compute Dashboard (click "Set as Active" on a node).',
120
+ );
121
+ }
122
+ return node;
123
+ }
124
+
125
+ async function handleTool(name, args) {
126
+ switch (name) {
127
+ case 'compute_info': {
128
+ const node = await requireActiveNode();
129
+ let gpuInfo = 'Unknown';
130
+ try {
131
+ gpuInfo = await ComputeNode.run({
132
+ nodeId: node.id,
133
+ command: 'nvidia-smi --query-gpu=name,memory.total,utilization.gpu --format=csv,noheader 2>/dev/null || echo "No GPU detected"',
134
+ skipSync: true,
135
+ });
136
+ } catch {
137
+ gpuInfo = 'Could not query GPU (connection error)';
138
+ }
139
+ return [
140
+ `Active Compute Node: ${node.name}`,
141
+ `Host: ${node.user}@${node.host}${node.port && node.port !== 22 ? ':' + node.port : ''}`,
142
+ `Type: ${node.type === 'slurm' ? 'Slurm HPC cluster' : 'Direct GPU server'}`,
143
+ `Work Directory: ${node.workDir || '~'}`,
144
+ `GPU Info:\n${gpuInfo}`,
145
+ ].join('\n');
146
+ }
147
+
148
+ case 'compute_run': {
149
+ const node = await requireActiveNode();
150
+ const output = await ComputeNode.run({
151
+ nodeId: node.id,
152
+ command: args.command,
153
+ cwd: args.cwd || undefined,
154
+ skipSync: args.skipSync !== false,
155
+ });
156
+ return output || '(no output)';
157
+ }
158
+
159
+ case 'compute_sync': {
160
+ const node = await requireActiveNode();
161
+ const output = await ComputeNode.sync({
162
+ nodeId: node.id,
163
+ direction: args.direction,
164
+ cwd: args.cwd,
165
+ files: args.files || [],
166
+ });
167
+ return output || `Sync ${args.direction} completed successfully.`;
168
+ }
169
+
170
+ case 'compute_slurm_submit': {
171
+ const node = await requireActiveNode();
172
+ const output = await ComputeNode.sbatch({
173
+ nodeId: node.id,
174
+ rawScript: args.script,
175
+ });
176
+ return output || 'Job submitted.';
177
+ }
178
+
179
+ case 'compute_slurm_queue': {
180
+ const node = await requireActiveNode();
181
+ const jobs = await ComputeNode.squeue({ nodeId: node.id });
182
+ if (jobs.length === 0) return 'No active jobs.';
183
+ const header = 'JobID\tName\tPartition\tState\tElapsed\tTime Limit';
184
+ const rows = jobs.map(
185
+ (j) => `${j.jobId}\t${j.name}\t${j.partition}\t${j.state}\t${j.elapsed}\t${j.timeLimit}`,
186
+ );
187
+ return [header, ...rows].join('\n');
188
+ }
189
+
190
+ default:
191
+ throw new Error(`Unknown tool: ${name}`);
192
+ }
193
+ }
194
+
195
+ // ─── MCP protocol handlers ───
196
+
197
+ server.setRequestHandler(
198
+ ListToolsRequestSchema,
199
+ async () => ({ tools: TOOLS }),
200
+ );
201
+
202
+ server.setRequestHandler(
203
+ CallToolRequestSchema,
204
+ async (request) => {
205
+ const { name, arguments: args } = request.params;
206
+ try {
207
+ const result = await handleTool(name, args || {});
208
+ return {
209
+ content: [{ type: 'text', text: result }],
210
+ };
211
+ } catch (error) {
212
+ return {
213
+ content: [{ type: 'text', text: `Error: ${error.message}` }],
214
+ isError: true,
215
+ };
216
+ }
217
+ },
218
+ );
219
+
220
+ // ─── Start server ───
221
+
222
+ const transport = new StdioServerTransport();
223
+ await server.connect(transport);
@@ -4,6 +4,25 @@ import { IS_PLATFORM } from '../constants/config.js';
4
4
 
5
5
  // Get JWT secret from environment or use default (for development)
6
6
  const JWT_SECRET = process.env.JWT_SECRET || 'claude-ui-dev-secret-change-in-production';
7
+ if (!process.env.JWT_SECRET && process.env.NODE_ENV === 'production') {
8
+ console.warn(
9
+ '[SECURITY] JWT_SECRET is not set — using the default development secret. '
10
+ + 'Tokens signed with this known-public secret are trivially forgeable. '
11
+ + 'Set JWT_SECRET in your environment before deploying to production.'
12
+ );
13
+ }
14
+
15
+ // Token lifetime (default: 30 days). Set JWT_EXPIRY to override, e.g. "24h", "30d".
16
+ const JWT_EXPIRY = process.env.JWT_EXPIRY || '30d';
17
+ // Validate JWT_EXPIRY at startup to fail fast on misconfiguration
18
+ try {
19
+ jwt.sign({ _validate: true }, 'test', { expiresIn: JWT_EXPIRY });
20
+ } catch {
21
+ throw new Error(
22
+ `Invalid JWT_EXPIRY value: "${JWT_EXPIRY}". `
23
+ + 'Use formats like "1h", "7d", "30d", or a number of seconds.'
24
+ );
25
+ }
7
26
 
8
27
  // Optional API key middleware
9
28
  const validateApiKey = (req, res, next) => {
@@ -54,15 +73,15 @@ const authenticateToken = async (req, res, next) => {
54
73
  }
55
74
  };
56
75
 
57
- // Generate JWT token (never expires)
76
+ // Generate JWT token with configurable expiry (default: 7 days)
58
77
  const generateToken = (user) => {
59
78
  return jwt.sign(
60
- {
61
- userId: user.id,
62
- username: user.username
79
+ {
80
+ userId: user.id,
81
+ username: user.username
63
82
  },
64
- JWT_SECRET
65
- // No expiration - token lasts forever
83
+ JWT_SECRET,
84
+ { expiresIn: JWT_EXPIRY }
66
85
  );
67
86
  };
68
87
 
@@ -0,0 +1,340 @@
1
+ /**
2
+ * Nano Claude Code provider — spawns the nano-claude-code CLI in stream-json harness mode.
3
+ * Uses the same WebSocket shapes as Claude (claude-response / claude-complete) for UI reuse.
4
+ *
5
+ * Requires a nano-claude-code build that supports:
6
+ * --output-format stream-json -p "..." --dangerously-skip-permissions
7
+ * --session-file <path.json> and --resume <path.json> for multi-turn persistence (absolute path under ~/.dr-claw/nano-sessions)
8
+ * result lines may include nano_session_file (optional)
9
+ * Install: https://github.com/OpenLAIR/nano-claude-code — pip install from that repo or use the published CLI name on PATH.
10
+ */
11
+
12
+ import { spawn } from 'child_process';
13
+ import crossSpawn from 'cross-spawn';
14
+ import crypto from 'crypto';
15
+ import { encodeProjectPath, ensureProjectSkillLinks } from './projects.js';
16
+ import { writeProjectTemplates } from './templates/index.js';
17
+ import { applyStageTagsToSession, recordIndexedSession } from './utils/sessionIndex.js';
18
+ import { ensureNanoDrClawSessionsRoot, resolveNanoSessionAbsPath } from './nanoSessionPaths.js';
19
+
20
+ const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn;
21
+
22
+ const activeNanoSessions = new Map();
23
+
24
+ let nanoShutdownHooksInstalled = false;
25
+
26
+ function installNanoProcessShutdownHooks() {
27
+ if (nanoShutdownHooksInstalled) {
28
+ return;
29
+ }
30
+ nanoShutdownHooksInstalled = true;
31
+ const shutdown = () => {
32
+ for (const { process: childProc } of activeNanoSessions.values()) {
33
+ try {
34
+ if (childProc && !childProc.killed) {
35
+ childProc.kill('SIGTERM');
36
+ }
37
+ } catch (_) {
38
+ // ignore
39
+ }
40
+ }
41
+ activeNanoSessions.clear();
42
+ };
43
+ process.on('exit', shutdown);
44
+ for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
45
+ process.on(sig, shutdown);
46
+ }
47
+ }
48
+
49
+ function resolveNanoCommand() {
50
+ const explicit = String(
51
+ process.env.NANO_CLAUDE_CODE_COMMAND || ''
52
+ ).trim();
53
+ if (explicit) {
54
+ return explicit;
55
+ }
56
+ return 'nano-claude-code';
57
+ }
58
+
59
+ async function persistNanoSessionMetadata(sessionId, projectPath, sessionMode) {
60
+ if (!sessionId || !projectPath) return;
61
+ try {
62
+ const { sessionDb } = await import('./database/db.js');
63
+ sessionDb.upsertSession(
64
+ sessionId,
65
+ encodeProjectPath(projectPath),
66
+ 'nano',
67
+ 'Nano Claude Code Session',
68
+ new Date().toISOString(),
69
+ 0,
70
+ { sessionMode: sessionMode || 'research', projectPath },
71
+ );
72
+ } catch (error) {
73
+ console.warn('[Nano] Failed to persist session metadata:', error.message);
74
+ }
75
+ }
76
+
77
+ export async function spawnNanoClaudeCode(command, options = {}, ws) {
78
+ installNanoProcessShutdownHooks();
79
+ const {
80
+ sessionId,
81
+ projectPath,
82
+ cwd,
83
+ model,
84
+ env,
85
+ sessionMode,
86
+ stageTagKeys,
87
+ stageTagSource = 'task_context',
88
+ } = options;
89
+
90
+ const workingDir = cwd || projectPath || process.cwd();
91
+
92
+ try {
93
+ await writeProjectTemplates(workingDir);
94
+ await ensureProjectSkillLinks(workingDir);
95
+ } catch (error) {
96
+ console.warn('[Nano] Project template setup:', error.message);
97
+ }
98
+
99
+ const streaming = String(
100
+ process.env.NANO_CLAUDE_CODE_STREAMING || ''
101
+ ).trim() === '1';
102
+
103
+ const isPlaceholderSession =
104
+ !sessionId ||
105
+ String(sessionId).startsWith('new-session-');
106
+
107
+ const capturedSessionId = isPlaceholderSession ? crypto.randomUUID() : String(sessionId);
108
+
109
+ await ensureNanoDrClawSessionsRoot();
110
+ const sessionAbsPath = resolveNanoSessionAbsPath(capturedSessionId);
111
+ if (!sessionAbsPath) {
112
+ const err = 'Invalid Nano Claude Code session id';
113
+ ws.send({ type: 'claude-error', error: err, sessionId: null });
114
+ return Promise.reject(new Error(err));
115
+ }
116
+
117
+ if (capturedSessionId && workingDir) {
118
+ applyStageTagsToSession({
119
+ sessionId: capturedSessionId,
120
+ projectPath: workingDir,
121
+ stageTagKeys,
122
+ source: stageTagSource,
123
+ });
124
+ }
125
+
126
+ if (isPlaceholderSession) {
127
+ recordIndexedSession({
128
+ sessionId: capturedSessionId,
129
+ provider: 'nano',
130
+ projectPath: workingDir,
131
+ sessionMode: sessionMode || 'research',
132
+ stageTagKeys,
133
+ tagSource: stageTagSource,
134
+ });
135
+ ws.send({
136
+ type: 'session-created',
137
+ sessionId: capturedSessionId,
138
+ provider: 'nano',
139
+ mode: sessionMode || 'research',
140
+ projectName: encodeProjectPath(workingDir),
141
+ });
142
+ }
143
+
144
+ if (ws.setSessionId && typeof ws.setSessionId === 'function') {
145
+ ws.setSessionId(capturedSessionId);
146
+ }
147
+
148
+ await persistNanoSessionMetadata(capturedSessionId, workingDir, sessionMode);
149
+
150
+ const nanoCmd = resolveNanoCommand();
151
+ const args = [
152
+ '--output-format', 'stream-json',
153
+ '-p', command,
154
+ '--dangerously-skip-permissions',
155
+ '--session-file', sessionAbsPath,
156
+ ];
157
+ if (!isPlaceholderSession) {
158
+ args.push('--resume', sessionAbsPath);
159
+ }
160
+ if (streaming) {
161
+ args.push('--streaming');
162
+ }
163
+ if (model) {
164
+ args.push('--model', model);
165
+ }
166
+
167
+ console.log('[Nano] spawn:', nanoCmd, args.join(' '));
168
+ console.log('[Nano] cwd:', workingDir);
169
+
170
+ return new Promise((resolve, reject) => {
171
+ const child = spawnFunction(nanoCmd, args, {
172
+ cwd: workingDir,
173
+ stdio: ['ignore', 'pipe', 'pipe'],
174
+ env: { ...(env || process.env) },
175
+ });
176
+
177
+ activeNanoSessions.set(capturedSessionId, { process: child, startTime: Date.now() });
178
+
179
+ const getSessionStartTime = () => activeNanoSessions.get(capturedSessionId)?.startTime;
180
+
181
+ let stdoutBuf = '';
182
+
183
+ child.stdout.on('data', (data) => {
184
+ stdoutBuf += data.toString();
185
+ const lines = stdoutBuf.split('\n');
186
+ stdoutBuf = lines.pop() || '';
187
+ for (const line of lines) {
188
+ const t = line.trim();
189
+ if (!t) continue;
190
+ try {
191
+ const response = JSON.parse(t);
192
+ switch (response.type) {
193
+ case 'assistant':
194
+ if (response.message) {
195
+ ws.send({
196
+ type: 'claude-response',
197
+ data: {
198
+ type: 'assistant',
199
+ message: response.message,
200
+ startTime: getSessionStartTime(),
201
+ },
202
+ sessionId: capturedSessionId,
203
+ });
204
+ }
205
+ break;
206
+ case 'user':
207
+ if (response.message) {
208
+ ws.send({
209
+ type: 'claude-response',
210
+ data: {
211
+ type: 'user',
212
+ message: response.message,
213
+ startTime: getSessionStartTime(),
214
+ },
215
+ sessionId: capturedSessionId,
216
+ });
217
+ }
218
+ break;
219
+ case 'stream_delta':
220
+ if (response.content && (response.delta_type === 'text' || response.delta_type === 'thinking')) {
221
+ ws.send({
222
+ type: 'claude-response',
223
+ data: {
224
+ type: 'content_block_delta',
225
+ startTime: getSessionStartTime(),
226
+ delta: { type: 'text_delta', text: response.content },
227
+ },
228
+ sessionId: capturedSessionId,
229
+ });
230
+ }
231
+ break;
232
+ case 'result': {
233
+ const u = response.usage || {};
234
+ const input = Number(u.input_tokens) || 0;
235
+ const output = Number(u.output_tokens) || 0;
236
+ const cacheCreate = Number(u.cache_creation_input_tokens) || 0;
237
+ const cacheRead = Number(u.cache_read_input_tokens) || 0;
238
+ if (input || output || cacheCreate || cacheRead) {
239
+ const total = parseInt(process.env.CONTEXT_WINDOW || process.env.VITE_CONTEXT_WINDOW || '200000', 10);
240
+ ws.send({
241
+ type: 'token-budget',
242
+ data: {
243
+ used: input + output + cacheCreate + cacheRead,
244
+ total,
245
+ breakdown: {
246
+ input,
247
+ output,
248
+ cacheCreation: cacheCreate,
249
+ cacheRead,
250
+ },
251
+ },
252
+ sessionId: capturedSessionId,
253
+ });
254
+ }
255
+ break;
256
+ }
257
+ default:
258
+ break;
259
+ }
260
+ } catch {
261
+ console.warn('[Nano] Ignoring non-JSON stdout line:', t.slice(0, 200));
262
+ }
263
+ }
264
+ });
265
+
266
+ child.stderr.on('data', (data) => {
267
+ const msg = data.toString().trim();
268
+ if (!msg) return;
269
+ console.error('[Nano] stderr:', msg);
270
+ ws.send({
271
+ type: 'claude-error',
272
+ error: msg.slice(0, 2000),
273
+ sessionId: capturedSessionId,
274
+ });
275
+ });
276
+
277
+ child.on('close', async (code) => {
278
+ activeNanoSessions.delete(capturedSessionId);
279
+ ws.send({
280
+ type: 'claude-complete',
281
+ sessionId: capturedSessionId,
282
+ exitCode: code,
283
+ isNewSession: isPlaceholderSession && Boolean(command && String(command).trim()),
284
+ });
285
+ if (code === 0) {
286
+ resolve();
287
+ } else {
288
+ reject(new Error(`nano-claude-code exited with code ${code}`));
289
+ }
290
+ });
291
+
292
+ child.on('error', (error) => {
293
+ activeNanoSessions.delete(capturedSessionId);
294
+ ws.send({
295
+ type: 'claude-error',
296
+ error: error.message,
297
+ sessionId: capturedSessionId,
298
+ });
299
+ reject(error);
300
+ });
301
+ });
302
+ }
303
+
304
+ export function abortNanoClaudeCodeSession(sessionId) {
305
+ const sessionData = activeNanoSessions.get(sessionId);
306
+ if (sessionData?.process) {
307
+ console.log(`[Nano] Aborting session: ${sessionId}`);
308
+ sessionData.process.kill('SIGTERM');
309
+ activeNanoSessions.delete(sessionId);
310
+ return true;
311
+ }
312
+ return false;
313
+ }
314
+
315
+ export function isNanoClaudeCodeSessionActive(sessionId) {
316
+ return activeNanoSessions.has(sessionId);
317
+ }
318
+
319
+ export function getNanoClaudeCodeSessionStartTime(sessionId) {
320
+ const sessionData = activeNanoSessions.get(sessionId);
321
+ return sessionData ? sessionData.startTime : null;
322
+ }
323
+
324
+ export function getActiveNanoClaudeCodeSessions() {
325
+ return Array.from(activeNanoSessions.keys());
326
+ }
327
+
328
+ /** Kill all in-flight Nano CLI children (e.g. before process exit). */
329
+ export function killAllNanoClaudeCodeChildren() {
330
+ for (const { process: childProc } of activeNanoSessions.values()) {
331
+ try {
332
+ if (childProc && !childProc.killed) {
333
+ childProc.kill('SIGTERM');
334
+ }
335
+ } catch (_) {
336
+ // ignore
337
+ }
338
+ }
339
+ activeNanoSessions.clear();
340
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Central Nano Claude Code session JSON location (~/.dr-claw/nano-sessions).
3
+ * Matches file watcher PROVIDER_WATCH_PATHS and avoids cwd-scattered drclaw-nano-*.json.
4
+ */
5
+
6
+ import fs from 'fs/promises';
7
+ import path from 'path';
8
+ import os from 'os';
9
+
10
+ export function getNanoDrClawSessionsRoot() {
11
+ return path.join(os.homedir(), '.dr-claw', 'nano-sessions');
12
+ }
13
+
14
+ export async function ensureNanoDrClawSessionsRoot() {
15
+ const root = getNanoDrClawSessionsRoot();
16
+ await fs.mkdir(root, { recursive: true });
17
+ return root;
18
+ }
19
+
20
+ /**
21
+ * Plain filename only: drclaw-nano-<sanitizedId>.json — no path segments / traversal.
22
+ */
23
+ export function safeNanoSessionFilename(sessionId) {
24
+ const safe = String(sessionId || '').replace(/[^a-zA-Z0-9._-]/g, '');
25
+ if (!safe || safe.includes('..')) {
26
+ return null;
27
+ }
28
+ const filename = `drclaw-nano-${safe}.json`;
29
+ if (path.basename(filename) !== filename) {
30
+ return null;
31
+ }
32
+ return filename;
33
+ }
34
+
35
+ export function resolveNanoSessionAbsPath(sessionId) {
36
+ const filename = safeNanoSessionFilename(sessionId);
37
+ if (!filename) {
38
+ return null;
39
+ }
40
+ return path.join(getNanoDrClawSessionsRoot(), filename);
41
+ }
@@ -22,6 +22,7 @@ import { applyStageTagsToSession, recordIndexedSession } from './utils/sessionIn
22
22
  import { classifyError, classifySDKError } from '../shared/errorClassifier.js';
23
23
  import { buildTempAttachmentFilename } from './utils/imageAttachmentFiles.js';
24
24
  import { buildCodexRealtimeTokenBudget } from './utils/sessionTokenUsage.js';
25
+ import { expandSkillCommand } from './utils/skillExpander.js';
25
26
 
26
27
  // Track active sessions
27
28
  const activeCodexSessions = new Map();
@@ -446,14 +447,17 @@ export async function queryCodex(command, options = {}, ws) {
446
447
 
447
448
  publishSessionId(thread.id || sessionId || null);
448
449
 
449
- const preparedInput = await prepareCodexInput(command, images, workingDirectory);
450
+ // Expand /skill-name slash commands into full SKILL.md instructions
451
+ const expandedCommand = await expandSkillCommand(command?.trim() || command, workingDirectory);
452
+
453
+ const preparedInput = await prepareCodexInput(expandedCommand, images, workingDirectory);
450
454
  tempImagePaths = preparedInput.tempImagePaths;
451
455
  tempDir = preparedInput.tempDir;
452
456
 
453
457
  // Execute with streaming
454
458
  // Prefer pre-uploaded attachments (buildCodexInput) over base64 temp images (prepareCodexInput)
455
459
  const codexInput = attachments
456
- ? buildCodexInput(command, attachments)
460
+ ? buildCodexInput(expandedCommand, attachments)
457
461
  : preparedInput.input;
458
462
  const streamedTurn = await thread.runStreamed(codexInput, {
459
463
  signal: abortController.signal