agent-office-cli 0.1.3 → 0.1.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-office-cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Run and manage AI agent sessions locally, with optional relay to agentoffice.top",
5
5
  "license": "MIT",
6
6
  "engines": {
@@ -1,31 +1,41 @@
1
+ const { displayZoneFor } = require("../state");
1
2
  const { GenericProvider } = require("./generic");
2
3
  const { findManagedCodexSessionFile, summarizeCodexSession } = require("./codex-transcript");
3
4
 
4
- const APPROVAL_PATTERNS = [
5
- "approval requested:",
6
- "approval requested by ",
7
- "tool call needs your approval",
8
- "requires approval by policy",
9
- "requires approval:"
5
+ const APPROVAL_LINE_PATTERNS = [
6
+ /^approval requested:/i,
7
+ /^approval requested by /i,
8
+ /^tool call needs your approval$/i,
9
+ /^requires approval by policy$/i,
10
+ /^requires approval:/i
10
11
  ];
11
12
 
12
- const IDLE_PATTERNS = [
13
- "conversation interrupted - tell the model what to do differently",
14
- "something went wrong? hit `/feedback` to",
15
- "something went wrong? hit /feedback to"
13
+ const IDLE_LINE_PATTERNS = [
14
+ /^conversation interrupted - tell the model what to do differently/i,
15
+ /^something went wrong\? hit `?\/feedback`? to/i
16
16
  ];
17
17
 
18
- const ATTENTION_PATTERNS = [
19
- "stream disconnected before completion",
20
- "error sending request for url",
21
- "network error",
22
- "connection timeout",
23
- "timed out",
24
- "failed to send request",
25
- "failed to submit",
26
- "panic"
18
+ const STATUS_LINE_CONTINUATION = String.raw`(?:$|[\s:.,;(])`;
19
+
20
+ const ATTENTION_LINE_PATTERNS = [
21
+ new RegExp(`^stream disconnected before completion${STATUS_LINE_CONTINUATION}`, "i"),
22
+ new RegExp(`^error sending request for url${STATUS_LINE_CONTINUATION}`, "i"),
23
+ new RegExp(`^network error${STATUS_LINE_CONTINUATION}`, "i"),
24
+ new RegExp(`^connection timeout${STATUS_LINE_CONTINUATION}`, "i"),
25
+ new RegExp(`^timed out${STATUS_LINE_CONTINUATION}`, "i"),
26
+ new RegExp(`^failed to send request${STATUS_LINE_CONTINUATION}`, "i"),
27
+ new RegExp(`^failed to submit${STATUS_LINE_CONTINUATION}`, "i"),
28
+ new RegExp(`^panic${STATUS_LINE_CONTINUATION}`, "i")
27
29
  ];
28
30
 
31
+ function matchesAnyLine(text, patterns) {
32
+ return String(text)
33
+ .split(/\r?\n/)
34
+ .map((line) => line.trim())
35
+ .filter(Boolean)
36
+ .some((line) => patterns.some((pattern) => pattern.test(line)));
37
+ }
38
+
29
39
  function activeOverlayPatch(session, nextLifecycleState) {
30
40
  if (!session || !["approval", "attention"].includes(session.displayState)) {
31
41
  return null;
@@ -72,23 +82,43 @@ class CodexProvider extends GenericProvider {
72
82
  }
73
83
 
74
84
  classifyOutput(chunk) {
75
- const text = String(chunk).toLowerCase();
76
-
77
- if (IDLE_PATTERNS.some((pattern) => text.includes(pattern))) {
85
+ if (matchesAnyLine(chunk, IDLE_LINE_PATTERNS)) {
78
86
  return "idle";
79
87
  }
80
88
 
81
- if (APPROVAL_PATTERNS.some((pattern) => text.includes(pattern))) {
89
+ if (matchesAnyLine(chunk, APPROVAL_LINE_PATTERNS)) {
82
90
  return "approval";
83
91
  }
84
92
 
85
- if (ATTENTION_PATTERNS.some((pattern) => text.includes(pattern))) {
93
+ if (matchesAnyLine(chunk, ATTENTION_LINE_PATTERNS)) {
86
94
  return "attention";
87
95
  }
88
96
 
89
97
  return null;
90
98
  }
91
99
 
100
+ getOverlayDisplayPatch(session, overlayState) {
101
+ if (!overlayState) {
102
+ if (session && session.displayState === "attention") {
103
+ const nextDisplayState = session.state || "working";
104
+ return {
105
+ displayState: nextDisplayState,
106
+ displayZone: displayZoneFor(nextDisplayState)
107
+ };
108
+ }
109
+ return null;
110
+ }
111
+
112
+ if (session && overlayState === session.displayState) {
113
+ return null;
114
+ }
115
+
116
+ return {
117
+ displayState: overlayState,
118
+ displayZone: displayZoneFor(overlayState)
119
+ };
120
+ }
121
+
92
122
  reconcileSession(session, context = {}) {
93
123
  if (session.status === "exited") {
94
124
  return null;
@@ -91,6 +91,19 @@ test("classifyOutput treats stream disconnects as attention", () => {
91
91
  assert.equal(nextState, "attention");
92
92
  });
93
93
 
94
+ test("classifyOutput ignores diagnostic text that only mentions attention patterns", () => {
95
+ const provider = new CodexProvider();
96
+ const nextState = provider.classifyOutput(
97
+ [
98
+ 'rg -n -i "conversation interrupted|error sending request for url|network error|timed out|failed to send request|failed to submit|panic|fetch failed"',
99
+ 'const nextState = provider.classifyOutput("network error: connection timed out", { meta: { codexSessionPath: "/tmp/mock-codex.jsonl" } });',
100
+ 'The changelog says stream disconnected before completion should surface as attention.'
101
+ ].join("\n")
102
+ );
103
+
104
+ assert.equal(nextState, null);
105
+ });
106
+
94
107
  test("classifyOutput does not treat plain explanatory approval text as a real approval prompt", () => {
95
108
  const provider = new CodexProvider();
96
109
  const nextState = provider.classifyOutput(
@@ -108,3 +121,20 @@ test("classifyOutput recognizes real Codex approval prompts", () => {
108
121
 
109
122
  assert.equal(nextState, "approval");
110
123
  });
124
+
125
+ test("getOverlayDisplayPatch clears stale attention overlays back to the lifecycle state", () => {
126
+ const provider = new CodexProvider();
127
+ const patch = provider.getOverlayDisplayPatch(
128
+ {
129
+ state: "working",
130
+ displayState: "attention",
131
+ displayZone: "attention-zone"
132
+ },
133
+ null
134
+ );
135
+
136
+ assert.deepEqual(patch, {
137
+ displayState: "working",
138
+ displayZone: "working-zone"
139
+ });
140
+ });
@@ -353,12 +353,21 @@ function createPtyManager({ store }) {
353
353
  const screen = await capturePane(runtime.tmuxSession);
354
354
  const latestSession = store.getSession(session.sessionId) || session;
355
355
  const overlayState = runtime.provider.classifyOutput(screen, latestSession);
356
-
357
- if (overlayState && overlayState !== latestSession.displayState) {
356
+ const overlayPatch = typeof runtime.provider.getOverlayDisplayPatch === "function"
357
+ ? runtime.provider.getOverlayDisplayPatch(latestSession, overlayState)
358
+ : (
359
+ overlayState && overlayState !== latestSession.displayState
360
+ ? {
361
+ displayState: overlayState,
362
+ displayZone: displayZoneFor(overlayState)
363
+ }
364
+ : null
365
+ );
366
+
367
+ if (overlayPatch) {
358
368
  store.setSessionState(session.sessionId, latestSession.state || "working", {
359
369
  status: "running",
360
- displayState: overlayState,
361
- displayZone: displayZoneFor(overlayState)
370
+ ...overlayPatch
362
371
  });
363
372
  }
364
373
 
package/src/tunnel.js CHANGED
@@ -3,6 +3,42 @@ const { toSessionSummary } = require("./core");
3
3
 
4
4
  const RECONNECT_BASE_MS = 1000;
5
5
  const RECONNECT_MAX_MS = 30000;
6
+ const LOCAL_PROXY_STRIP_HEADERS = new Set([
7
+ "accept-encoding",
8
+ "connection",
9
+ "content-length",
10
+ "cookie",
11
+ "origin",
12
+ "referer",
13
+ "te",
14
+ "trailer",
15
+ "transfer-encoding",
16
+ "upgrade"
17
+ ]);
18
+
19
+ function shouldStripLocalProxyHeader(name) {
20
+ return (
21
+ LOCAL_PROXY_STRIP_HEADERS.has(name) ||
22
+ name.startsWith("proxy-") ||
23
+ name.startsWith("sec-") ||
24
+ name.startsWith("x-forwarded-")
25
+ );
26
+ }
27
+
28
+ function buildLocalRequestHeaders(headers, localServerUrl) {
29
+ const nextHeaders = {};
30
+
31
+ for (const [name, rawValue] of Object.entries(headers || {})) {
32
+ const key = String(name).toLowerCase();
33
+ if (shouldStripLocalProxyHeader(key) || rawValue == null) {
34
+ continue;
35
+ }
36
+ nextHeaders[key] = Array.isArray(rawValue) ? rawValue.join(", ") : String(rawValue);
37
+ }
38
+
39
+ nextHeaders.host = new URL(localServerUrl).host;
40
+ return nextHeaders;
41
+ }
6
42
 
7
43
  function createTunnelClient({ key, relayUrl, localServerUrl }) {
8
44
  let ws = null;
@@ -116,7 +152,7 @@ function createTunnelClient({ key, relayUrl, localServerUrl }) {
116
152
  const fetchUrl = `${localServerUrl}${msg.path}`;
117
153
  const fetchOptions = {
118
154
  method: msg.method || "GET",
119
- headers: { ...msg.headers, host: new URL(localServerUrl).host }
155
+ headers: buildLocalRequestHeaders(msg.headers, localServerUrl)
120
156
  };
121
157
  if (msg.body && msg.method !== "GET" && msg.method !== "HEAD") {
122
158
  fetchOptions.body = msg.body;
@@ -220,5 +256,6 @@ function createTunnelClient({ key, relayUrl, localServerUrl }) {
220
256
  }
221
257
 
222
258
  module.exports = {
259
+ buildLocalRequestHeaders,
223
260
  createTunnelClient
224
261
  };
@@ -0,0 +1,31 @@
1
+ const test = require("node:test");
2
+ const assert = require("node:assert/strict");
3
+
4
+ const { buildLocalRequestHeaders } = require("./tunnel");
5
+
6
+ test("buildLocalRequestHeaders strips browser-only proxy headers and rewrites host", () => {
7
+ const next = buildLocalRequestHeaders(
8
+ {
9
+ authorization: "Bearer token",
10
+ accept: "*/*",
11
+ "content-type": "application/json",
12
+ host: "agentoffice.top",
13
+ connection: "keep-alive",
14
+ "accept-encoding": "gzip, deflate, br, zstd",
15
+ "content-length": "123",
16
+ origin: "https://agentoffice.top",
17
+ referer: "https://agentoffice.top/office",
18
+ "sec-ch-ua": "\"Chromium\";v=\"146\"",
19
+ "sec-fetch-mode": "cors",
20
+ "x-forwarded-for": "203.0.113.10"
21
+ },
22
+ "http://127.0.0.1:8765"
23
+ );
24
+
25
+ assert.deepEqual(next, {
26
+ authorization: "Bearer token",
27
+ accept: "*/*",
28
+ "content-type": "application/json",
29
+ host: "127.0.0.1:8765"
30
+ });
31
+ });