noctrace 0.8.2 → 1.0.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 +2 -0
- package/bin/docker-watcher.sh +108 -0
- package/bin/noctrace-mcp.js +47 -8
- package/bin/noctrace.js +123 -0
- package/dist/client/assets/index-D3XepZ5e.js +30 -0
- package/dist/client/index.html +1 -1
- package/dist/server/server/docker.js +152 -0
- package/dist/server/server/routes/api.js +100 -1
- package/dist/server/shared/filter.js +5 -0
- package/dist/server/shared/parser.js +100 -0
- package/hooks/hooks.json +9 -9
- package/package.json +1 -1
- package/dist/client/assets/index-B37clQwh.js +0 -30
package/dist/client/index.html
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
8
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
9
9
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&display=swap" rel="stylesheet" />
|
|
10
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
+
<script type="module" crossorigin src="/assets/index-D3XepZ5e.js"></script>
|
|
11
11
|
<link rel="stylesheet" crossorigin href="/assets/index-DwPuae45.css">
|
|
12
12
|
</head>
|
|
13
13
|
<body>
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docker support for noctrace.
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates container inspection, HTTP-tool detection, host URL resolution,
|
|
5
|
+
* watcher injection, and cleanup. All Docker commands go through the
|
|
6
|
+
* DockerRunner interface so callers (and tests) can swap in a stub.
|
|
7
|
+
*/
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Default runner (real child_process)
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
import { execFileSync, spawn as nodeSpawn } from 'node:child_process';
|
|
12
|
+
export const defaultDockerRunner = {
|
|
13
|
+
execSync(cmd, args, opts = {}) {
|
|
14
|
+
return execFileSync(cmd, args, { stdio: opts.stdio ?? 'pipe', timeout: opts.timeout })
|
|
15
|
+
.toString();
|
|
16
|
+
},
|
|
17
|
+
spawn(cmd, args, opts = {}) {
|
|
18
|
+
return nodeSpawn(cmd, args, opts);
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Validation
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
const CONTAINER_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/;
|
|
25
|
+
/**
|
|
26
|
+
* Returns true when the container name is syntactically safe to pass as an
|
|
27
|
+
* argv element. Rejects names that could be used for command injection or
|
|
28
|
+
* path traversal.
|
|
29
|
+
*/
|
|
30
|
+
export function isValidContainerName(name) {
|
|
31
|
+
return CONTAINER_NAME_RE.test(name);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Returns true when a path is free of directory-traversal sequences.
|
|
35
|
+
* Any segment equal to `..` is rejected regardless of surrounding context.
|
|
36
|
+
*/
|
|
37
|
+
export function isValidContainerPath(p) {
|
|
38
|
+
return !p.split('/').includes('..');
|
|
39
|
+
}
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Container state
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
/**
|
|
44
|
+
* Verify the container is running. Throws with a user-friendly message when
|
|
45
|
+
* the container is not found or not running.
|
|
46
|
+
*/
|
|
47
|
+
export function assertContainerRunning(containerArg, runner) {
|
|
48
|
+
try {
|
|
49
|
+
runner.execSync('docker', ['inspect', '--format', '{{.State.Running}}', containerArg], { stdio: 'pipe' });
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
throw new Error(`Container "${containerArg}" not found or not running.\nCheck: docker ps`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Claude config dir inside container
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
/**
|
|
59
|
+
* Ask the container for the Claude config directory (respects CLAUDE_CONFIG_DIR).
|
|
60
|
+
*/
|
|
61
|
+
export function resolveClaudeDir(containerArg, runner) {
|
|
62
|
+
return runner
|
|
63
|
+
.execSync('docker', ['exec', containerArg, 'sh', '-c', 'echo ${CLAUDE_CONFIG_DIR:-$HOME/.claude}'], { stdio: 'pipe' })
|
|
64
|
+
.trim();
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Determine which HTTP client is available inside the container.
|
|
68
|
+
* Tries curl first, falls back to wget, returns 'none' if neither is present.
|
|
69
|
+
*/
|
|
70
|
+
export function detectHttpTool(containerArg, runner) {
|
|
71
|
+
try {
|
|
72
|
+
runner.execSync('docker', ['exec', containerArg, 'which', 'curl'], { stdio: 'pipe' });
|
|
73
|
+
return 'curl';
|
|
74
|
+
}
|
|
75
|
+
catch { /* curl not found */ }
|
|
76
|
+
try {
|
|
77
|
+
runner.execSync('docker', ['exec', containerArg, 'which', 'wget'], { stdio: 'pipe' });
|
|
78
|
+
return 'wget';
|
|
79
|
+
}
|
|
80
|
+
catch { /* wget not found */ }
|
|
81
|
+
return 'none';
|
|
82
|
+
}
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Host URL resolution
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
/**
|
|
87
|
+
* Resolve the URL the container can use to reach the host.
|
|
88
|
+
*
|
|
89
|
+
* Priority:
|
|
90
|
+
* 1. `host.docker.internal` (works on macOS/Windows Docker Desktop)
|
|
91
|
+
* 2. Gateway IP from `docker inspect` (Linux Docker)
|
|
92
|
+
* 3. Fallback to `host.docker.internal` (best-effort)
|
|
93
|
+
*/
|
|
94
|
+
export function resolveHostUrl(containerArg, runner) {
|
|
95
|
+
try {
|
|
96
|
+
runner.execSync('docker', ['exec', containerArg, 'getent', 'hosts', 'host.docker.internal'], { stdio: 'pipe' });
|
|
97
|
+
return 'http://host.docker.internal';
|
|
98
|
+
}
|
|
99
|
+
catch { /* not available — try gateway IP */ }
|
|
100
|
+
try {
|
|
101
|
+
const gatewayIp = runner
|
|
102
|
+
.execSync('docker', [
|
|
103
|
+
'inspect',
|
|
104
|
+
'--format',
|
|
105
|
+
'{{range .NetworkSettings.Networks}}{{.Gateway}}{{end}}',
|
|
106
|
+
containerArg,
|
|
107
|
+
], { stdio: 'pipe' })
|
|
108
|
+
.trim();
|
|
109
|
+
if (gatewayIp) {
|
|
110
|
+
return `http://${gatewayIp}`;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch { /* ignore */ }
|
|
114
|
+
return 'http://host.docker.internal';
|
|
115
|
+
}
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// Watcher injection
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
/**
|
|
120
|
+
* Copy a local script file into the container at `/tmp/noctrace-watcher.sh`,
|
|
121
|
+
* mark it executable, and return. The caller is responsible for running it.
|
|
122
|
+
*/
|
|
123
|
+
export function copyWatcherScript(containerArg, localScriptPath, runner) {
|
|
124
|
+
runner.execSync('docker', ['cp', localScriptPath, `${containerArg}:/tmp/noctrace-watcher.sh`], { stdio: 'pipe' });
|
|
125
|
+
runner.execSync('docker', ['exec', containerArg, 'chmod', '+x', '/tmp/noctrace-watcher.sh'], { stdio: 'pipe' });
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Start the injected watcher script inside the container in the background.
|
|
129
|
+
* Returns the spawned process handle (callers can swallow its errors).
|
|
130
|
+
*/
|
|
131
|
+
export function spawnWatcher(containerArg, claudeDir, containerTargetUrl, runner) {
|
|
132
|
+
const proc = runner.spawn('docker', [
|
|
133
|
+
'exec', '-d', containerArg,
|
|
134
|
+
'sh', '-c', '/tmp/noctrace-watcher.sh "$1" "$2" "$3"', '--',
|
|
135
|
+
claudeDir, containerTargetUrl, containerArg,
|
|
136
|
+
], { stdio: 'ignore' });
|
|
137
|
+
proc.on('error', () => { });
|
|
138
|
+
return proc;
|
|
139
|
+
}
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// Cleanup
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
/**
|
|
144
|
+
* Kill the noctrace-watcher process inside the container.
|
|
145
|
+
* Safe to call after container exit — errors are swallowed.
|
|
146
|
+
*/
|
|
147
|
+
export function cleanupWatcher(containerArg, runner) {
|
|
148
|
+
try {
|
|
149
|
+
runner.execSync('docker', ['exec', containerArg, 'sh', '-c', 'pkill -f noctrace-watcher 2>/dev/null || true'], { stdio: 'pipe', timeout: 3000 });
|
|
150
|
+
}
|
|
151
|
+
catch { /* container may be gone */ }
|
|
152
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* REST API routes for project and session data.
|
|
3
3
|
* All data is read from JSONL files on disk — no in-memory caching.
|
|
4
4
|
*/
|
|
5
|
-
import { Router } from 'express';
|
|
5
|
+
import express, { Router } from 'express';
|
|
6
6
|
import fs from 'node:fs/promises';
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import { WebSocket } from 'ws';
|
|
@@ -76,6 +76,8 @@ export function buildApiRouter(claudeHome, wss) {
|
|
|
76
76
|
* When non-empty the client operates in "MCP mode" and shows only these sessions.
|
|
77
77
|
*/
|
|
78
78
|
const registeredSessionPaths = new Set();
|
|
79
|
+
/** Last heartbeat timestamp from Docker container watchers, keyed by container name. */
|
|
80
|
+
const dockerHeartbeats = new Map();
|
|
79
81
|
/** Broadcast a message to all connected WebSocket clients. */
|
|
80
82
|
function broadcast(msg) {
|
|
81
83
|
const payload = JSON.stringify(msg);
|
|
@@ -543,6 +545,103 @@ export function buildApiRouter(claudeHome, wss) {
|
|
|
543
545
|
}
|
|
544
546
|
});
|
|
545
547
|
// ---------------------------------------------------------------------------
|
|
548
|
+
// POST /api/docker/stream
|
|
549
|
+
// ---------------------------------------------------------------------------
|
|
550
|
+
/**
|
|
551
|
+
* Receive streamed JSONL content from a Docker container watcher.
|
|
552
|
+
* Appends raw text to a local sync file under the projects directory.
|
|
553
|
+
* Chokidar picks up the file change and handles parsing + WebSocket broadcasting.
|
|
554
|
+
*/
|
|
555
|
+
router.post('/docker/stream', express.text({ type: 'text/plain', limit: '1mb' }), async (req, res) => {
|
|
556
|
+
try {
|
|
557
|
+
const containerName = req.headers['x-container-name'];
|
|
558
|
+
const containerPath = req.headers['x-container-path'];
|
|
559
|
+
if (typeof containerName !== 'string' || !containerName) {
|
|
560
|
+
res.status(400).json({ error: 'X-Container-Name header required' });
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
if (typeof containerPath !== 'string' || !containerPath) {
|
|
564
|
+
res.status(400).json({ error: 'X-Container-Path header required' });
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(containerName)) {
|
|
568
|
+
res.status(400).json({ error: 'Invalid container name format' });
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
const body = req.body;
|
|
572
|
+
if (!body || !body.trim()) {
|
|
573
|
+
res.status(400).json({ error: 'Empty body' });
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
// Extract relative path after /projects/
|
|
577
|
+
const projectsIdx = containerPath.indexOf('/projects/');
|
|
578
|
+
if (projectsIdx === -1) {
|
|
579
|
+
res.status(400).json({ error: 'Container path must contain /projects/' });
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
const relativePath = containerPath.slice(projectsIdx + '/projects/'.length);
|
|
583
|
+
// Reject path traversal patterns in the relative path
|
|
584
|
+
if (relativePath.includes('..')) {
|
|
585
|
+
res.status(400).json({ error: 'Invalid path' });
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
const slashIdx = relativePath.indexOf('/');
|
|
589
|
+
if (slashIdx === -1) {
|
|
590
|
+
res.status(400).json({ error: 'Invalid container path structure' });
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
const containerSlug = relativePath.slice(0, slashIdx);
|
|
594
|
+
const sessionFile = relativePath.slice(slashIdx + 1);
|
|
595
|
+
const localSlug = `docker--${containerName}--${containerSlug}`;
|
|
596
|
+
const localPath = path.join(projectsDir, localSlug, sessionFile);
|
|
597
|
+
try {
|
|
598
|
+
assertWithinBase(localPath, projectsDir);
|
|
599
|
+
}
|
|
600
|
+
catch {
|
|
601
|
+
res.status(400).json({ error: 'Invalid path' });
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
await fs.mkdir(path.dirname(localPath), { recursive: true });
|
|
605
|
+
await fs.appendFile(localPath, body.endsWith('\n') ? body : body + '\n');
|
|
606
|
+
// Auto-register the session
|
|
607
|
+
if (localPath.endsWith('.jsonl') && !registeredSessionPaths.has(localPath)) {
|
|
608
|
+
const resolvedPath = path.resolve(localPath);
|
|
609
|
+
registeredSessionPaths.add(resolvedPath);
|
|
610
|
+
broadcast({ type: 'session-registered', sessionPath: resolvedPath });
|
|
611
|
+
}
|
|
612
|
+
res.json({ ok: true });
|
|
613
|
+
}
|
|
614
|
+
catch (err) {
|
|
615
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
616
|
+
res.status(500).json({ error: message });
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
// ---------------------------------------------------------------------------
|
|
620
|
+
// POST /api/docker/heartbeat
|
|
621
|
+
// ---------------------------------------------------------------------------
|
|
622
|
+
/** Keepalive endpoint for Docker container watchers. */
|
|
623
|
+
router.post('/docker/heartbeat', (req, res) => {
|
|
624
|
+
const containerName = req.headers['x-container-name'];
|
|
625
|
+
if (typeof containerName !== 'string' || !containerName) {
|
|
626
|
+
res.status(400).json({ error: 'X-Container-Name header required' });
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
dockerHeartbeats.set(containerName, Date.now());
|
|
630
|
+
res.json({ ok: true });
|
|
631
|
+
});
|
|
632
|
+
// ---------------------------------------------------------------------------
|
|
633
|
+
// GET /api/docker/status
|
|
634
|
+
// ---------------------------------------------------------------------------
|
|
635
|
+
/** Returns the status of connected Docker containers. */
|
|
636
|
+
router.get('/docker/status', (_req, res) => {
|
|
637
|
+
const containers = [];
|
|
638
|
+
const now = Date.now();
|
|
639
|
+
for (const [name, ts] of dockerHeartbeats) {
|
|
640
|
+
containers.push({ name, lastHeartbeat: ts, stale: now - ts > 30_000 });
|
|
641
|
+
}
|
|
642
|
+
res.json({ containers });
|
|
643
|
+
});
|
|
644
|
+
// ---------------------------------------------------------------------------
|
|
546
645
|
// POST /api/hooks
|
|
547
646
|
// ---------------------------------------------------------------------------
|
|
548
647
|
/**
|
|
@@ -67,6 +67,11 @@ export function parseFilterString(filter) {
|
|
|
67
67
|
result.typeFilters.push('agent');
|
|
68
68
|
continue;
|
|
69
69
|
}
|
|
70
|
+
// 'turn' keyword: treated as type filter
|
|
71
|
+
if (lower === 'turn') {
|
|
72
|
+
result.typeFilters.push('turn');
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
70
75
|
// Everything else is free text
|
|
71
76
|
result.textTokens.push(lower);
|
|
72
77
|
}
|
|
@@ -636,6 +636,106 @@ export function parseJsonlContent(content) {
|
|
|
636
636
|
});
|
|
637
637
|
}
|
|
638
638
|
}
|
|
639
|
+
// Create turn rows for user prompts (string content = human text, not tool results)
|
|
640
|
+
for (const rec of records) {
|
|
641
|
+
if (rec.type !== 'user')
|
|
642
|
+
continue;
|
|
643
|
+
const ur = rec;
|
|
644
|
+
// Array content means tool_result records — skip
|
|
645
|
+
if (Array.isArray(ur.message.content))
|
|
646
|
+
continue;
|
|
647
|
+
if (ur.isMeta === true)
|
|
648
|
+
continue;
|
|
649
|
+
const raw = rec;
|
|
650
|
+
if (raw['isSynthetic'] === true)
|
|
651
|
+
continue;
|
|
652
|
+
const text = typeof ur.message.content === 'string' ? ur.message.content : '';
|
|
653
|
+
if (!text.trim())
|
|
654
|
+
continue;
|
|
655
|
+
const ts = new Date(ur.timestamp).getTime();
|
|
656
|
+
const truncated = text.length > 120 ? text.slice(0, 117) + '...' : text;
|
|
657
|
+
top.push({
|
|
658
|
+
id: `turn-user-${ur.uuid}`,
|
|
659
|
+
type: 'turn',
|
|
660
|
+
toolName: 'UserPrompt',
|
|
661
|
+
label: truncated,
|
|
662
|
+
startTime: ts,
|
|
663
|
+
endTime: ts,
|
|
664
|
+
duration: 0,
|
|
665
|
+
status: 'success',
|
|
666
|
+
parentAgentId: null,
|
|
667
|
+
input: {},
|
|
668
|
+
output: text,
|
|
669
|
+
inputTokens: 0,
|
|
670
|
+
outputTokens: 0,
|
|
671
|
+
tokenDelta: 0,
|
|
672
|
+
contextFillPercent: 0,
|
|
673
|
+
isReread: false,
|
|
674
|
+
isFailure: false,
|
|
675
|
+
children: [],
|
|
676
|
+
tips: [],
|
|
677
|
+
modelName: null,
|
|
678
|
+
estimatedCost: null,
|
|
679
|
+
agentType: null,
|
|
680
|
+
agentColor: null,
|
|
681
|
+
sequence: null,
|
|
682
|
+
isFastMode: false,
|
|
683
|
+
parentToolUseId: null,
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
// Create turn rows for assistant text-only responses (no tool_use blocks)
|
|
687
|
+
for (const rec of records) {
|
|
688
|
+
if (rec.type !== 'assistant')
|
|
689
|
+
continue;
|
|
690
|
+
const ar = rec;
|
|
691
|
+
const hasToolUse = ar.message.content.some(b => isToolUse(b));
|
|
692
|
+
if (hasToolUse)
|
|
693
|
+
continue; // already handled by the tool row creation loop
|
|
694
|
+
if (ar.message.error)
|
|
695
|
+
continue; // already handled by api-error loop
|
|
696
|
+
const texts = ar.message.content
|
|
697
|
+
.filter((b) => b.type === 'text')
|
|
698
|
+
.map(b => b.text);
|
|
699
|
+
const fullText = texts.join('\n');
|
|
700
|
+
if (!fullText.trim())
|
|
701
|
+
continue;
|
|
702
|
+
const ts = new Date(ar.timestamp).getTime();
|
|
703
|
+
const truncated = fullText.length > 120 ? fullText.slice(0, 117) + '...' : fullText;
|
|
704
|
+
const usage = ar.message.usage;
|
|
705
|
+
const inputTokens = (usage?.input_tokens ?? 0)
|
|
706
|
+
+ (usage?.cache_creation_input_tokens ?? 0)
|
|
707
|
+
+ (usage?.cache_read_input_tokens ?? 0);
|
|
708
|
+
const outputTokens = usage?.output_tokens ?? 0;
|
|
709
|
+
const modelName = typeof ar.message.model === 'string' ? ar.message.model : null;
|
|
710
|
+
top.push({
|
|
711
|
+
id: `turn-asst-${ar.uuid}`,
|
|
712
|
+
type: 'turn',
|
|
713
|
+
toolName: 'AssistantResponse',
|
|
714
|
+
label: truncated,
|
|
715
|
+
startTime: ts,
|
|
716
|
+
endTime: ts,
|
|
717
|
+
duration: 0,
|
|
718
|
+
status: 'success',
|
|
719
|
+
parentAgentId: null,
|
|
720
|
+
input: {},
|
|
721
|
+
output: fullText,
|
|
722
|
+
inputTokens,
|
|
723
|
+
outputTokens,
|
|
724
|
+
tokenDelta: 0,
|
|
725
|
+
contextFillPercent: (inputTokens / effectiveWindow) * 100,
|
|
726
|
+
isReread: false,
|
|
727
|
+
isFailure: false,
|
|
728
|
+
children: [],
|
|
729
|
+
tips: [],
|
|
730
|
+
modelName,
|
|
731
|
+
estimatedCost: null,
|
|
732
|
+
agentType: null,
|
|
733
|
+
agentColor: null,
|
|
734
|
+
sequence: typeof ar.sequence === 'number' ? ar.sequence : null,
|
|
735
|
+
isFastMode: ar.message.speed === 'fast',
|
|
736
|
+
parentToolUseId: null,
|
|
737
|
+
});
|
|
738
|
+
}
|
|
639
739
|
return top;
|
|
640
740
|
}
|
|
641
741
|
/**
|
package/hooks/hooks.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"hooks": [
|
|
5
5
|
{
|
|
6
6
|
"type": "command",
|
|
7
|
-
"command": "curl -s -X POST http://localhost:4117/api/hooks -H 'Content-Type: application/json' --data-raw \"$(cat)\"",
|
|
7
|
+
"command": "curl -s -X POST \"${NOCTRACE_URL:-http://localhost:4117}/api/hooks\" -H 'Content-Type: application/json' --data-raw \"$(cat)\"",
|
|
8
8
|
"async": true
|
|
9
9
|
}
|
|
10
10
|
]
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"hooks": [
|
|
16
16
|
{
|
|
17
17
|
"type": "command",
|
|
18
|
-
"command": "curl -s -X POST http://localhost:4117/api/hooks -H 'Content-Type: application/json' --data-raw \"$(cat)\"",
|
|
18
|
+
"command": "curl -s -X POST \"${NOCTRACE_URL:-http://localhost:4117}/api/hooks\" -H 'Content-Type: application/json' --data-raw \"$(cat)\"",
|
|
19
19
|
"async": true
|
|
20
20
|
}
|
|
21
21
|
]
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"hooks": [
|
|
27
27
|
{
|
|
28
28
|
"type": "command",
|
|
29
|
-
"command": "curl -s -X POST http://localhost:4117/api/hooks -H 'Content-Type: application/json' --data-raw \"$(cat)\"",
|
|
29
|
+
"command": "curl -s -X POST \"${NOCTRACE_URL:-http://localhost:4117}/api/hooks\" -H 'Content-Type: application/json' --data-raw \"$(cat)\"",
|
|
30
30
|
"async": true
|
|
31
31
|
}
|
|
32
32
|
]
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"hooks": [
|
|
38
38
|
{
|
|
39
39
|
"type": "command",
|
|
40
|
-
"command": "curl -s -X POST http://localhost:4117/api/hooks -H 'Content-Type: application/json' --data-raw \"$(cat)\"",
|
|
40
|
+
"command": "curl -s -X POST \"${NOCTRACE_URL:-http://localhost:4117}/api/hooks\" -H 'Content-Type: application/json' --data-raw \"$(cat)\"",
|
|
41
41
|
"async": true
|
|
42
42
|
}
|
|
43
43
|
]
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"hooks": [
|
|
49
49
|
{
|
|
50
50
|
"type": "command",
|
|
51
|
-
"command": "curl -s -X POST http://localhost:4117/api/hooks -H 'Content-Type: application/json' --data-raw \"$(cat)\"",
|
|
51
|
+
"command": "curl -s -X POST \"${NOCTRACE_URL:-http://localhost:4117}/api/hooks\" -H 'Content-Type: application/json' --data-raw \"$(cat)\"",
|
|
52
52
|
"async": true
|
|
53
53
|
}
|
|
54
54
|
]
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"hooks": [
|
|
60
60
|
{
|
|
61
61
|
"type": "command",
|
|
62
|
-
"command": "curl -s -X POST http://localhost:4117/api/hooks -H 'Content-Type: application/json' --data-raw \"$(cat)\"",
|
|
62
|
+
"command": "curl -s -X POST \"${NOCTRACE_URL:-http://localhost:4117}/api/hooks\" -H 'Content-Type: application/json' --data-raw \"$(cat)\"",
|
|
63
63
|
"async": true
|
|
64
64
|
}
|
|
65
65
|
]
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"hooks": [
|
|
71
71
|
{
|
|
72
72
|
"type": "command",
|
|
73
|
-
"command": "curl -s -X POST http://localhost:4117/api/hooks -H 'Content-Type: application/json' --data-raw \"$(cat)\"",
|
|
73
|
+
"command": "curl -s -X POST \"${NOCTRACE_URL:-http://localhost:4117}/api/hooks\" -H 'Content-Type: application/json' --data-raw \"$(cat)\"",
|
|
74
74
|
"async": true
|
|
75
75
|
}
|
|
76
76
|
]
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
"hooks": [
|
|
82
82
|
{
|
|
83
83
|
"type": "command",
|
|
84
|
-
"command": "curl -s -X POST http://localhost:4117/api/hooks -H 'Content-Type: application/json' --data-raw \"$(cat)\"",
|
|
84
|
+
"command": "curl -s -X POST \"${NOCTRACE_URL:-http://localhost:4117}/api/hooks\" -H 'Content-Type: application/json' --data-raw \"$(cat)\"",
|
|
85
85
|
"async": true
|
|
86
86
|
}
|
|
87
87
|
]
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
"hooks": [
|
|
93
93
|
{
|
|
94
94
|
"type": "command",
|
|
95
|
-
"command": "curl -s -X POST http://localhost:4117/api/hooks -H 'Content-Type: application/json' --data-raw \"$(cat)\"",
|
|
95
|
+
"command": "curl -s -X POST \"${NOCTRACE_URL:-http://localhost:4117}/api/hooks\" -H 'Content-Type: application/json' --data-raw \"$(cat)\"",
|
|
96
96
|
"async": true
|
|
97
97
|
}
|
|
98
98
|
]
|
package/package.json
CHANGED