noctrace 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -143,6 +143,7 @@ No config files. No cloud. Everything stays local. Optional hooks for richer rea
143
143
  | CLI Flag | Description |
144
144
  |----------|-------------|
145
145
  | `--docker <container>` | Attach to a running Docker container and stream its Claude Code sessions back to your host. Zero container setup |
146
+ | `--devcontainer <path>` | Resolve the running devcontainer for a local folder path and attach to it. Pass `.` for the current directory. Falls back to `--docker` if you pass a container name directly |
146
147
  | `--install-hooks` | Configure Claude Code to push real-time events to noctrace |
147
148
  | `--uninstall-hooks` | Remove noctrace hooks from Claude Code |
148
149
 
package/bin/noctrace.js CHANGED
@@ -197,14 +197,11 @@ if (args.includes('--disable')) {
197
197
  process.exit(0);
198
198
  }
199
199
 
200
- if (args.includes('--docker')) {
201
- const containerArg = args[args.indexOf('--docker') + 1];
202
- if (!containerArg || containerArg.startsWith('--')) {
203
- console.error('[noctrace] Usage: npx noctrace --docker <container-name-or-id>');
204
- console.error('[noctrace] Example: npx noctrace --docker my-claude-container');
205
- process.exit(1);
206
- }
207
-
200
+ /**
201
+ * Run Docker watcher mode with a known, already-validated container ID.
202
+ * Shared by --docker and --devcontainer.
203
+ */
204
+ async function runDockerMode(containerArg) {
208
205
  const {
209
206
  isValidContainerName,
210
207
  assertContainerRunning,
@@ -320,6 +317,44 @@ if (args.includes('--docker')) {
320
317
  await new Promise(() => {});
321
318
  }
322
319
 
320
+ if (args.includes('--docker')) {
321
+ const containerArg = args[args.indexOf('--docker') + 1];
322
+ if (!containerArg || containerArg.startsWith('--')) {
323
+ console.error('[noctrace] Usage: npx noctrace --docker <container-name-or-id>');
324
+ console.error('[noctrace] Example: npx noctrace --docker my-claude-container');
325
+ process.exit(1);
326
+ }
327
+ await runDockerMode(containerArg);
328
+ }
329
+
330
+ if (args.includes('--devcontainer')) {
331
+ const devcontainerArg = args[args.indexOf('--devcontainer') + 1];
332
+ if (!devcontainerArg || devcontainerArg.startsWith('--')) {
333
+ console.error('[noctrace] Usage: npx noctrace --devcontainer <path-or-container>');
334
+ console.error('[noctrace] Examples:');
335
+ console.error('[noctrace] npx noctrace --devcontainer .');
336
+ console.error('[noctrace] npx noctrace --devcontainer /Users/me/myproject');
337
+ console.error('[noctrace] npx noctrace --devcontainer my-devcontainer-id');
338
+ process.exit(1);
339
+ }
340
+
341
+ const { resolveDevcontainerContainer, defaultDockerRunner } =
342
+ await import('../dist/server/server/docker.js');
343
+
344
+ let resolvedContainer;
345
+ try {
346
+ resolvedContainer = resolveDevcontainerContainer(devcontainerArg, defaultDockerRunner);
347
+ } catch (err) {
348
+ const lines = err.message.split('\n');
349
+ for (const line of lines) {
350
+ console.error(`[noctrace] ${line}`);
351
+ }
352
+ process.exit(1);
353
+ }
354
+
355
+ await runDockerMode(resolvedContainer);
356
+ }
357
+
323
358
  if (args.includes('--mcp')) {
324
359
  // MCP mode: boot the Express server and speak JSON-RPC over stdio.
325
360
  // stdout is the JSON-RPC channel — all logging must go to stderr.
@@ -5,6 +5,8 @@
5
5
  * watcher injection, and cleanup. All Docker commands go through the
6
6
  * DockerRunner interface so callers (and tests) can swap in a stub.
7
7
  */
8
+ import path from 'node:path';
9
+ import os from 'node:os';
8
10
  // ---------------------------------------------------------------------------
9
11
  // Default runner (real child_process)
10
12
  // ---------------------------------------------------------------------------
@@ -150,3 +152,66 @@ export function cleanupWatcher(containerArg, runner) {
150
152
  }
151
153
  catch { /* container may be gone */ }
152
154
  }
155
+ // ---------------------------------------------------------------------------
156
+ // Devcontainer support
157
+ // ---------------------------------------------------------------------------
158
+ /**
159
+ * Look up a running container by an exact Docker label match.
160
+ * Returns the container ID (short form), or null when nothing matches.
161
+ *
162
+ * Uses `docker ps --filter "label=<label>=<value>" --format "{{.ID}}"`.
163
+ * The label and value are passed as a single `label=key=value` filter argument
164
+ * so no shell interpolation occurs.
165
+ */
166
+ export function findContainerByLabel(label, value, runner) {
167
+ let output;
168
+ try {
169
+ output = runner.execSync('docker', ['ps', '--filter', `label=${label}=${value}`, '--format', '{{.ID}}'], { stdio: 'pipe' });
170
+ }
171
+ catch {
172
+ return null;
173
+ }
174
+ const id = output.trim().split('\n')[0]?.trim() ?? '';
175
+ return id.length > 0 ? id : null;
176
+ }
177
+ /**
178
+ * Resolve a devcontainer argument to a concrete container ID.
179
+ *
180
+ * If `input` looks like a path (starts with `/`, `.`, `./`, or `~/`) it is
181
+ * resolved to an absolute path and looked up via the canonical
182
+ * `devcontainer.local_folder` label, falling back to the older
183
+ * `vsch.local.folder` label. When neither label matches an error is thrown
184
+ * with a clear hint pointing the user at `docker ps --filter "label=devcontainer.*"`.
185
+ *
186
+ * If `input` is not a path it is treated as a container name/ID.
187
+ * `isValidContainerName` is checked and the value is returned directly.
188
+ *
189
+ * @param cwd - Working directory used to resolve relative paths. Defaults to `process.cwd()`.
190
+ */
191
+ export function resolveDevcontainerContainer(input, runner, cwd) {
192
+ const isPath = input.startsWith('/') || input.startsWith('.') || input.startsWith('~/');
193
+ if (!isPath) {
194
+ if (!isValidContainerName(input)) {
195
+ throw new Error(`Invalid container name: "${input}"`);
196
+ }
197
+ return input;
198
+ }
199
+ // Resolve to an absolute path — devcontainer labels always store absolute paths.
200
+ // path.resolve does not expand ~ so handle that explicitly.
201
+ let absPath;
202
+ if (input.startsWith('~/')) {
203
+ absPath = path.join(os.homedir(), input.slice(2));
204
+ }
205
+ else {
206
+ absPath = path.resolve(cwd ?? process.cwd(), input);
207
+ }
208
+ // Try canonical label first, then the older VS Code label.
209
+ const id = findContainerByLabel('devcontainer.local_folder', absPath, runner) ??
210
+ findContainerByLabel('vsch.local.folder', absPath, runner);
211
+ if (id === null) {
212
+ throw new Error(`No devcontainer found for path: ${absPath}\n` +
213
+ `Hint: make sure the devcontainer is running, then check:\n` +
214
+ ` docker ps --filter "label=devcontainer.local_folder"`);
215
+ }
216
+ return id;
217
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "noctrace",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Claude Code observability — DevTools-style waterfall visualizer for AI agent workflows, token tracking, and context health monitoring",
5
5
  "type": "module",
6
6
  "license": "MIT",