nterminal 1.3.3 → 1.3.5

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 (114) hide show
  1. package/bin/nterminal.js +8 -3
  2. package/dist/client/assets/MarkdownPreview-BIrwUAaa.js +1 -0
  3. package/dist/client/assets/TranscriptMarkdownBody-CI0ztvMX.js +3 -0
  4. package/dist/client/assets/{index-DDCpqDNb.js → index-EN8ngMgv.js} +1 -1
  5. package/dist/client/assets/index-JQJ1Eet1.js +58 -0
  6. package/dist/client/assets/index-xkL4h3qt.css +1 -0
  7. package/dist/client/index.html +2 -2
  8. package/dist/scripts/hooks.js +83 -0
  9. package/dist/scripts/hooks.js.map +1 -0
  10. package/dist/server/accounts/accountService.d.ts +23 -1
  11. package/dist/server/accounts/accountService.js +235 -103
  12. package/dist/server/accounts/accountService.js.map +1 -1
  13. package/dist/server/accounts/providerRequests.d.ts +12 -0
  14. package/dist/server/accounts/providerRequests.js +159 -0
  15. package/dist/server/accounts/providerRequests.js.map +1 -0
  16. package/dist/server/agent/agentRoutes.js +11 -0
  17. package/dist/server/agent/agentRoutes.js.map +1 -1
  18. package/dist/server/hooks/helperJournal.d.ts +16 -0
  19. package/dist/server/hooks/helperJournal.js +74 -0
  20. package/dist/server/hooks/helperJournal.js.map +1 -0
  21. package/dist/server/hooks/hookBinding.d.ts +39 -0
  22. package/dist/server/hooks/hookBinding.js +91 -0
  23. package/dist/server/hooks/hookBinding.js.map +1 -0
  24. package/dist/server/hooks/hookFiles.d.ts +24 -0
  25. package/dist/server/hooks/hookFiles.js +160 -0
  26. package/dist/server/hooks/hookFiles.js.map +1 -0
  27. package/dist/server/hooks/hookInstallation.d.ts +79 -0
  28. package/dist/server/hooks/hookInstallation.js +978 -0
  29. package/dist/server/hooks/hookInstallation.js.map +1 -0
  30. package/dist/server/hooks/hookProcess.d.ts +5 -0
  31. package/dist/server/hooks/hookProcess.js +85 -0
  32. package/dist/server/hooks/hookProcess.js.map +1 -0
  33. package/dist/server/hooks/hookReceiver.d.ts +66 -0
  34. package/dist/server/hooks/hookReceiver.js +549 -0
  35. package/dist/server/hooks/hookReceiver.js.map +1 -0
  36. package/dist/server/hooks/lifecycle.d.ts +40 -0
  37. package/dist/server/hooks/lifecycle.js +200 -0
  38. package/dist/server/hooks/lifecycle.js.map +1 -0
  39. package/dist/server/hooks/publicationLock.d.ts +3 -0
  40. package/dist/server/hooks/publicationLock.js +137 -0
  41. package/dist/server/hooks/publicationLock.js.map +1 -0
  42. package/dist/server/hooks/verifiedLock.d.ts +24 -0
  43. package/dist/server/hooks/verifiedLock.js +207 -0
  44. package/dist/server/hooks/verifiedLock.js.map +1 -0
  45. package/dist/server/http.d.ts +4 -0
  46. package/dist/server/http.js +6 -0
  47. package/dist/server/http.js.map +1 -1
  48. package/dist/server/index.js +69 -2
  49. package/dist/server/index.js.map +1 -1
  50. package/dist/server/notifications/agentCompletionMonitor.d.ts +16 -1
  51. package/dist/server/notifications/agentCompletionMonitor.js +261 -47
  52. package/dist/server/notifications/agentCompletionMonitor.js.map +1 -1
  53. package/dist/server/notifications/completionNotificationRouter.d.ts +2 -0
  54. package/dist/server/notifications/completionNotificationRouter.js +9 -0
  55. package/dist/server/notifications/completionNotificationRouter.js.map +1 -1
  56. package/dist/server/routes/accountRoutes.js +11 -0
  57. package/dist/server/routes/accountRoutes.js.map +1 -1
  58. package/dist/server/routes/activityRoutes.d.ts +13 -0
  59. package/dist/server/routes/activityRoutes.js +165 -0
  60. package/dist/server/routes/activityRoutes.js.map +1 -0
  61. package/dist/server/routes/agentManagementRoutes.js +8 -1
  62. package/dist/server/routes/agentManagementRoutes.js.map +1 -1
  63. package/dist/server/routes/hookRoutes.d.ts +16 -0
  64. package/dist/server/routes/hookRoutes.js +77 -0
  65. package/dist/server/routes/hookRoutes.js.map +1 -0
  66. package/dist/server/routes/terminalRoutes.js +4 -0
  67. package/dist/server/routes/terminalRoutes.js.map +1 -1
  68. package/dist/server/routes/transcriptQuery.d.ts +2 -1
  69. package/dist/server/routes/transcriptQuery.js +3 -1
  70. package/dist/server/routes/transcriptQuery.js.map +1 -1
  71. package/dist/server/terminal/PtyAdapter.d.ts +18 -0
  72. package/dist/server/terminal/PtyAdapter.js.map +1 -1
  73. package/dist/server/terminal/TerminalManager.d.ts +61 -0
  74. package/dist/server/terminal/TerminalManager.js +665 -12
  75. package/dist/server/terminal/TerminalManager.js.map +1 -1
  76. package/dist/server/terminal/TmuxPtyAdapter.d.ts +14 -2
  77. package/dist/server/terminal/TmuxPtyAdapter.js +104 -16
  78. package/dist/server/terminal/TmuxPtyAdapter.js.map +1 -1
  79. package/dist/server/terminal/activityChannel.d.ts +57 -0
  80. package/dist/server/terminal/activityChannel.js +166 -0
  81. package/dist/server/terminal/activityChannel.js.map +1 -0
  82. package/dist/server/terminal/agentTranscriptStatus.d.ts +12 -0
  83. package/dist/server/terminal/agentTranscriptStatus.js +78 -7
  84. package/dist/server/terminal/agentTranscriptStatus.js.map +1 -1
  85. package/dist/server/terminal/codexTranscriptSource.d.ts +2 -0
  86. package/dist/server/terminal/codexTranscriptSource.js +53 -46
  87. package/dist/server/terminal/codexTranscriptSource.js.map +1 -1
  88. package/dist/server/terminal/subagentSources.js +2 -1
  89. package/dist/server/terminal/subagentSources.js.map +1 -1
  90. package/dist/server/terminal/transcriptHeader.d.ts +85 -0
  91. package/dist/server/terminal/transcriptHeader.js +376 -0
  92. package/dist/server/terminal/transcriptHeader.js.map +1 -0
  93. package/dist/server/terminal/transcriptHistory.d.ts +16 -2
  94. package/dist/server/terminal/transcriptHistory.js +120 -21
  95. package/dist/server/terminal/transcriptHistory.js.map +1 -1
  96. package/dist/server/terminal/transcriptStore.d.ts +6 -0
  97. package/dist/server/terminal/transcriptStore.js +222 -26
  98. package/dist/server/terminal/transcriptStore.js.map +1 -1
  99. package/dist/shared/activityProbe.d.ts +10 -0
  100. package/dist/shared/activityProbe.js +24 -0
  101. package/dist/shared/activityProbe.js.map +1 -0
  102. package/dist/shared/protocol.d.ts +45 -0
  103. package/dist/shared/protocol.js.map +1 -1
  104. package/dist/shared/types.d.ts +9 -1
  105. package/docs/features.md +10 -0
  106. package/docs/provider-hooks.md +287 -0
  107. package/docs/terminal-history.md +10 -0
  108. package/package.json +2 -1
  109. package/scripts/nterminal-hook.cjs +171 -0
  110. package/scripts/nterminalctl +26 -2
  111. package/dist/client/assets/MarkdownPreview-CWK2qIZp.js +0 -1
  112. package/dist/client/assets/TranscriptMarkdownBody-Ddi-rw6X.js +0 -3
  113. package/dist/client/assets/index-CcAQThtj.js +0 -56
  114. package/dist/client/assets/index-DTTX24tu.css +0 -1
@@ -0,0 +1,171 @@
1
+ 'use strict';
2
+
3
+ // Standalone installer payload: copy to <stateDir>/hooks/forward.cjs. No app imports.
4
+ const fs = require('node:fs');
5
+ const fsp = require('node:fs/promises');
6
+ const http = require('node:http');
7
+ const path = require('node:path');
8
+ const { randomUUID } = require('node:crypto');
9
+
10
+ const MAX_INPUT = 4 * 1024 * 1024;
11
+ const ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}$/;
12
+ const ERRORS = new Set(['authentication_error', 'rate_limit_error', 'api_error', 'overloaded_error',
13
+ 'server_error', 'timeout', 'permission_denied', 'cancelled', 'provider_error', 'rate_limit', 'overloaded',
14
+ 'authentication_failed', 'oauth_org_not_allowed', 'account_on_hold', 'billing_error', 'invalid_request',
15
+ 'model_not_found', 'max_output_tokens', 'cloud_credential_error', 'unknown']);
16
+ let request;
17
+ let finished = false;
18
+ const deadline = setTimeout(finish, 700);
19
+
20
+ function finish() {
21
+ if (finished) return;
22
+ finished = true;
23
+ clearTimeout(deadline);
24
+ request?.destroy();
25
+ // A hook must never request continuation, grant permission, or fail the provider.
26
+ try { fs.writeSync(1, '{}\n'); } catch {}
27
+ process.exit(0);
28
+ }
29
+ process.on('uncaughtException', finish);
30
+ process.on('unhandledRejection', finish);
31
+ process.stdin.on('error', finish);
32
+ process.stdout.on('error', finish);
33
+
34
+ function options() {
35
+ const args = process.argv.slice(2);
36
+ const result = { notify: false };
37
+ while (args.length) {
38
+ const key = args.shift();
39
+ if (key === '--provider' && result.provider === undefined) result.provider = args.shift();
40
+ else if (key === '--endpoint' && result.endpoint === undefined) result.endpoint = args.shift();
41
+ else if (key === '--notify' && !result.notify) result.notify = true;
42
+ else if (result.notify && args.length === 0 && result.raw === undefined) result.raw = key;
43
+ else throw new Error('arguments');
44
+ }
45
+ if (!['codex', 'claude'].includes(result.provider) || !result.endpoint || !path.isAbsolute(result.endpoint)
46
+ || (result.notify && (result.provider !== 'codex' || result.raw === undefined))) throw new Error('arguments');
47
+ return result;
48
+ }
49
+
50
+ async function readPrivate(file, limit) {
51
+ const handle = await fsp.open(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
52
+ try {
53
+ const info = await handle.stat();
54
+ if (!info.isFile() || info.uid !== process.getuid() || (info.mode & 0o777) !== 0o600 || info.size > limit) throw new Error('file');
55
+ const buffer = Buffer.alloc(limit + 1);
56
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
57
+ if (bytesRead > limit) throw new Error('size');
58
+ return buffer.subarray(0, bytesRead).toString('utf8');
59
+ } finally { await handle.close(); }
60
+ }
61
+
62
+ async function endpoint(file) {
63
+ if (path.basename(file) !== 'endpoint.json') throw new Error('endpoint');
64
+ const directory = path.dirname(file);
65
+ const info = await fsp.lstat(directory);
66
+ if (!info.isDirectory() || info.uid !== process.getuid() || (info.mode & 0o777) !== 0o700
67
+ || await fsp.realpath(directory) !== directory) throw new Error('directory');
68
+ const value = JSON.parse(await readPrivate(file, 4096));
69
+ if (value.version !== 1 || !/^[0-9a-f]{32}$/.test(value.generation) || !/^[0-9a-f]{64}$/.test(value.token)
70
+ || value.socketPath !== path.join(directory, `r-${value.generation}.sock`)
71
+ || Buffer.byteLength(value.socketPath) > 103) throw new Error('endpoint');
72
+ const socket = await fsp.lstat(value.socketPath);
73
+ if (!socket.isSocket() || socket.uid !== process.getuid() || (socket.mode & 0o777) !== 0o600) throw new Error('socket');
74
+ return value;
75
+ }
76
+
77
+ async function ancestry() {
78
+ const chain = [];
79
+ const seen = new Set();
80
+ let pid = process.pid;
81
+ while (pid > 0 && chain.length < 64 && !seen.has(pid)) {
82
+ seen.add(pid);
83
+ const raw = await fsp.readFile(`/proc/${pid}/stat`, 'utf8');
84
+ if (raw.length > 4096 || !raw.startsWith(`${pid} (`)) throw new Error('stat');
85
+ const fields = raw.slice(raw.lastIndexOf(')') + 2).trim().split(/\s+/);
86
+ if (!/^\d{1,20}$/.test(fields[19] || '') || !/^\d+$/.test(fields[1] || '')
87
+ || fields[0] === 'Z' || fields[0] === 'X') throw new Error('stat');
88
+ chain.push({ pid, startTicks: fields[19] });
89
+ pid = Number(fields[1]);
90
+ }
91
+ if (pid !== 0 || chain.length < 2) throw new Error('chain');
92
+ return chain;
93
+ }
94
+
95
+ function metadata(raw, provider, notify) {
96
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) throw new Error('input');
97
+ const event = notify ? raw.type : raw.hook_event_name;
98
+ const sessionId = notify ? raw['thread-id'] : raw.session_id;
99
+ if (typeof event !== 'string' || !/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(event)
100
+ || (notify && event !== 'agent-turn-complete') || typeof sessionId !== 'string' || !ID.test(sessionId)) throw new Error('identity');
101
+ // Stamp the provider's spawn time, not the post-startup time: async (non-blocking)
102
+ // tool hooks may finish after a later synchronous Stop, and the receiver orders
103
+ // events by this timestamp. performance.timeOrigin is this process's start time.
104
+ const started = Math.min(Date.now(), Math.floor(performance.timeOrigin));
105
+ const result = { provider, event, sessionId, pid: process.pid, occurredAt: new Date(started).toISOString(), eventId: randomUUID() };
106
+ for (const [key, value] of Object.entries({ transcriptPath: raw.transcript_path, cwd: raw.cwd,
107
+ turnId: notify ? raw['turn-id'] : raw.turn_id, promptId: raw.prompt_id, agentId: raw.agent_id })) {
108
+ if (value === undefined || (provider === 'codex' && key === 'transcriptPath' && value === null)) continue;
109
+ if (typeof value !== 'string') throw new Error('metadata');
110
+ if (key === 'cwd' || key === 'transcriptPath') {
111
+ if (value.length > 4096 || !path.isAbsolute(value) || /[\x00-\x1f\x7f]/.test(value)) throw new Error('path');
112
+ } else if (!ID.test(value)) throw new Error('id');
113
+ result[key] = value;
114
+ }
115
+ for (const [key, value] of Object.entries({ background: raw.background, stopHookActive: raw.stop_hook_active, hasBackgroundTasks: raw.has_background_tasks })) {
116
+ if (value === undefined) continue;
117
+ if (typeof value !== 'boolean') throw new Error('flag');
118
+ result[key] = value;
119
+ }
120
+ if (provider === 'claude' && (event === 'Stop' || event === 'SubagentStop')) {
121
+ const tasks = raw.background_tasks;
122
+ const crons = raw.session_crons;
123
+ for (const value of [tasks, crons]) {
124
+ if (value !== undefined && !Array.isArray(value)) throw new Error('tasks');
125
+ }
126
+ // Only presence matters. Missing registry data is unknown; never copy task or prompt bodies.
127
+ if (tasks?.length || crons?.length) result.hasBackgroundTasks = true;
128
+ else if (tasks !== undefined && crons !== undefined && result.hasBackgroundTasks === undefined) result.hasBackgroundTasks = false;
129
+ }
130
+ if (raw.error !== undefined) result.error = typeof raw.error === 'string' && ERRORS.has(raw.error) ? raw.error : 'provider_error';
131
+ if (raw.notification_type !== undefined) {
132
+ if (typeof raw.notification_type !== 'string' || !/^[a-z][a-z0-9_]{0,63}$/.test(raw.notification_type)) throw new Error('notification');
133
+ result.notificationType = raw.notification_type;
134
+ }
135
+ return result;
136
+ }
137
+
138
+ async function stdin() {
139
+ let bytes = 0;
140
+ let chunks = [];
141
+ for await (const chunk of process.stdin) {
142
+ bytes += chunk.length;
143
+ if (bytes <= MAX_INPUT) chunks.push(chunk);
144
+ else chunks = []; // Drain overflow without keeping prompt/tool content in memory.
145
+ }
146
+ if (bytes > MAX_INPUT) throw new Error('size');
147
+ return Buffer.concat(chunks, bytes).toString('utf8');
148
+ }
149
+
150
+ async function main() {
151
+ const opts = options();
152
+ const input = opts.notify ? opts.raw : await stdin();
153
+ if (Buffer.byteLength(input) > MAX_INPUT) return;
154
+ const event = metadata(JSON.parse(input), opts.provider, opts.notify);
155
+ const destination = await endpoint(opts.endpoint);
156
+ event.ancestors = await ancestry();
157
+ const body = JSON.stringify(event);
158
+ if (Buffer.byteLength(body) > 64 * 1024 || finished) return;
159
+ await new Promise(resolve => {
160
+ request = http.request({ socketPath: destination.socketPath, method: 'POST', path: '/hook', agent: false,
161
+ headers: { authorization: `Bearer ${destination.token}`, 'content-type': 'application/json', 'content-length': Buffer.byteLength(body), connection: 'close' } }, response => {
162
+ response.resume();
163
+ response.once('end', resolve);
164
+ response.once('error', resolve);
165
+ });
166
+ request.once('error', resolve);
167
+ request.end(body);
168
+ });
169
+ }
170
+
171
+ main().then(finish, finish);
@@ -790,6 +790,26 @@ clean_state_paths() {
790
790
  rm -rf "$state_dir/transcript-cache"
791
791
  }
792
792
 
793
+ # Best effort and never fatal: remove only NTerminal-owned provider hook entries
794
+ # (the installer verifies ownership from its manifests). Must run before .env is
795
+ # removed because CODEX_HOME/CLAUDE_CONFIG_DIR may be defined there.
796
+ remove_provider_hooks() {
797
+ local entry="$APP_DIR/dist/scripts/hooks.js"
798
+ local provider
799
+ if [[ ! -f "$entry" ]] || ! command -v "$NODE_BIN" >/dev/null 2>&1; then
800
+ info "warning: provider hooks were not checked (hooks CLI unavailable); run 'nterminal hooks remove --provider codex|claude' before uninstalling the package"
801
+ return 0
802
+ fi
803
+ for provider in codex claude; do
804
+ if "$NODE_BIN" "$entry" remove --provider "$provider" >/dev/null 2>&1; then
805
+ info "provider hooks removed provider=$provider"
806
+ else
807
+ info "warning: provider hooks may remain provider=$provider; check with: nterminal hooks status --provider $provider"
808
+ fi
809
+ done
810
+ return 0
811
+ }
812
+
793
813
  cmd_clean() {
794
814
  local force=0
795
815
  local remove_env=0
@@ -816,6 +836,7 @@ cmd_clean() {
816
836
 
817
837
  local state_dir
818
838
  state_dir="$(dirname "$STATE_PATH")"
839
+ remove_provider_hooks || true
819
840
  clean_state_paths "$STATE_PATH" "$PID_PATH" "$LOG_PATH" "$MONITOR_PID_PATH"
820
841
  if (( remove_env == 1 )); then
821
842
  rm -f "$ENV_FILE"
@@ -846,6 +867,7 @@ cmd_uninstall() {
846
867
  if (( kill_tmux == 1 )) && command -v tmux >/dev/null 2>&1; then
847
868
  tmux -L nterminal kill-server >/dev/null 2>&1 || true
848
869
  fi
870
+ remove_provider_hooks || true
849
871
  clean_state_paths "$STATE_PATH" "$PID_PATH" "$LOG_PATH" "$MONITOR_PID_PATH"
850
872
  rm -f "$ENV_FILE"
851
873
  remove_owned_launchd_plist
@@ -939,10 +961,12 @@ Commands:
939
961
  Stop the supervisor and start again after an npm package update.
940
962
  build No-op for package installs; verifies the bundled runtime exists.
941
963
  deploy Restart the installed package using the bundled runtime.
942
- clean --force Stop server, kill NTerminal tmux sessions, and remove local state/log/pid files.
964
+ clean --force Stop server, kill NTerminal tmux sessions, remove NTerminal's provider hooks,
965
+ and remove local state/log/pid files.
943
966
  Add --env to also remove .env. Add --keep-tmux to preserve live panes.
944
967
  uninstall --force
945
- Stop server, unload NTerminal launchd agent, remove local runtime files and .env.
968
+ Stop server, unload NTerminal launchd agent, remove NTerminal's provider hooks,
969
+ local runtime files and .env.
946
970
  Add --keep-tmux to preserve live panes.
947
971
  logs [N] Show the last N log lines. Default: 100.
948
972
  logs -f [N] Follow logs.
@@ -1 +0,0 @@
1
- import{j as a}from"./index-CcAQThtj.js";import{M as e,r as o}from"./index-DDCpqDNb.js";function s({content:r}){return a.jsx(e,{remarkPlugins:[o],children:r})}export{s as default};
@@ -1,3 +0,0 @@
1
- import{j as o,R as a}from"./index-CcAQThtj.js";import{M as u,r as f}from"./index-DDCpqDNb.js";function k({text:r,searchQuery:t}){return o.jsx(u,{components:p(t),remarkPlugins:[f],children:j(r)})}function p(r){return{a({node:t,children:n,...e}){return o.jsx("a",{...e,target:"_blank",rel:"noreferrer",children:d(n,r)})},blockquote({node:t,children:n,...e}){return o.jsx("blockquote",{...e,children:d(n,r)})},code({node:t,children:n,...e}){return o.jsx("code",{...e,children:d(n,r)})},em({node:t,children:n,...e}){return o.jsx("em",{...e,children:d(n,r)})},h1({node:t,children:n,...e}){return o.jsx("h1",{...e,children:d(n,r)})},h2({node:t,children:n,...e}){return o.jsx("h2",{...e,children:d(n,r)})},h3({node:t,children:n,...e}){return o.jsx("h3",{...e,children:d(n,r)})},h4({node:t,children:n,...e}){return o.jsx("h4",{...e,children:d(n,r)})},h5({node:t,children:n,...e}){return o.jsx("h5",{...e,children:d(n,r)})},h6({node:t,children:n,...e}){return o.jsx("h6",{...e,children:d(n,r)})},li({node:t,children:n,...e}){return o.jsx("li",{...e,children:d(n,r)})},p({node:t,children:n,...e}){return o.jsx("p",{...e,children:d(n,r)})},pre({node:t,children:n,...e}){return o.jsx("pre",{...e,className:"transcript-markdown-code",children:n})},strong({node:t,children:n,...e}){return o.jsx("strong",{...e,children:d(n,r)})},td({node:t,children:n,...e}){return o.jsx("td",{...e,children:d(n,r)})},th({node:t,children:n,...e}){return o.jsx("th",{...e,children:d(n,r)})}}}function d(r,t){return a.Children.map(r,n=>typeof n=="string"?m(n,t):n)}function m(r,t){const n=t.trim();if(!n)return r||" ";const e=r.toLowerCase(),s=n.toLowerCase(),c=[];let l=0,i=e.indexOf(s);for(;i!==-1;){i>l&&c.push(r.slice(l,i));const h=i+n.length;c.push(o.jsx("mark",{className:"transcript-search-hit",children:r.slice(i,h)},`${i}-${h}`)),l=h,i=e.indexOf(s,l)}return l<r.length&&c.push(r.slice(l)),c.length>0?c:r||" "}function j(r){const t=r.split(`
2
- `);let n=!1,e=null;return t.map((s,c)=>{const l=n,i=/^ {0,3}(```+|~~~+)/.exec(s);if(i){const h=i[1]?.startsWith("`")?"`":"~";n?e===h&&(n=!1,e=null):(n=!0,e=h)}return c===t.length-1||l||i||s.trim()===""||s.endsWith(" ")?s:`${s.replace(/[ \t]+$/,"")} `}).join(`
3
- `)}export{k as default};