axmap-cli 1.7.0 โ†’ 1.9.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/mcp/server.mjs CHANGED
@@ -46,6 +46,7 @@ import path from 'node:path'
46
46
  import readline from 'node:readline'
47
47
  import { fileURLToPath } from 'node:url'
48
48
  import { syncConfiguredProfile } from '../src/identity.mjs'
49
+ import { resolveTarget } from '../src/workspace.mjs'
49
50
 
50
51
  const HERE = path.dirname(fileURLToPath(import.meta.url))
51
52
 
@@ -77,14 +78,26 @@ let rootsTask = null
77
78
  let rootsGeneration = 0
78
79
  let rootRequestId = 0
79
80
  const pendingRoots = new Map()
80
- let REPO = resolveRepo()
81
-
82
- function gitRoot(cwd) {
83
- const r = spawnSync('git', ['rev-parse', '--show-toplevel'], { cwd, encoding: 'utf8', windowsHide: true })
84
- const top = r.status === 0 ? (r.stdout ?? '').trim() : ''
85
- return top ? fs.realpathSync(top) : null
81
+ /**
82
+ * ๐Ÿ”ด ์‹œ์ž‘ ํด๋”๊ฐ€ ์—ฌ๋Ÿฌ ์ €์žฅ์†Œ๋ฅผ ๋‹ด์€ ์ž‘์—… ํด๋”๋ผ ๋ชจํ˜ธํ•˜๋ฉด ์—ฌ๊ธฐ์„œ ์ฃฝ์ง€ ์•Š๋Š”๋‹ค.
83
+ * ์„œ๋ฒ„๊ฐ€ ๋– ์•ผ ๋„๊ตฌ ํ˜ธ์ถœ์ด ๊ทธ ๋ชจํ˜ธํ•จ๊ณผ ๊ณ ๋ฅด๋Š” ๋ฐฉ๋ฒ•์„ ์—์ด์ „ํŠธ์—๊ฒŒ ์•Œ๋ ค ์ค„ ์ˆ˜ ์žˆ๋‹ค.
84
+ * ๋Œ€์ƒ์€ ๋„๊ตฌ๋ฅผ ๋ถ€๋ฅผ ๋•Œ๋งˆ๋‹ค ๋‹ค์‹œ ์ •ํ•˜๋ฏ€๋กœ, ์‚ฌ๋žŒ์ด `.axmap.json` ์„ ์“ฐ๋ฉด ๋‹ค์Œ
85
+ * ํ˜ธ์ถœ๋ถ€ํ„ฐ ๋ฐ”๋กœ ๋”ฐ๋ฅธ๋‹ค.
86
+ */
87
+ let REPO
88
+ try { REPO = resolveRepo() } catch (e) {
89
+ REPO = process.cwd()
90
+ process.stderr.write(`axMap MCP: ๋Œ€์ƒ ์ €์žฅ์†Œ๋ฅผ ์•„์ง ์ •ํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.\n${e.message}\n`)
86
91
  }
87
92
 
93
+ /**
94
+ * ํด๋” โ†’ ๋Œ€์ƒ ์ €์žฅ์†Œ. ์œ„๋กœ ์ฐพ๊ธฐ(Git ์•ˆ)๋ฅผ ๋จผ์ € ํ•˜๊ณ , Git ๋ฐ–์ด๋ฉด ์ž‘์—… ํด๋” ๊ทœ์น™
95
+ * (`.axmap.json` ์ˆ˜๋™ ์„ค์ • โ†’ ๋ฐ”๋กœ ์•„๋ž˜ ์ €์žฅ์†Œ ์ž๋™ ํƒ์ƒ‰ โ†’ ๋ชจํ˜ธํ•˜๋ฉด ์˜ค๋ฅ˜)์„ ์“ด๋‹ค.
96
+ * ๊ทœ์น™์€ `src/workspace.mjs` ํ•œ ๊ณณ์— ์žˆ๋‹ค โ€” CLIยท๋กœ์ปฌ ์„ธ์…˜ ์—ฐ๊ฒฐ๊ณผ ๊ฐ™์€ ๋‹ต์„ ๋‚ด์•ผ ํ•œ๋‹ค.
97
+ * ์ €์žฅ์†Œ๊ฐ€ ์—†์œผ๋ฉด null, ๋ชจํ˜ธํ•˜๊ฑฐ๋‚˜ ํŒ์ •ํ•˜์ง€ ๋ชปํ•˜๋ฉด ์˜ˆ์™ธ๋‹ค.
98
+ */
99
+ function gitRoot(cwd) { return resolveTarget(cwd) }
100
+
88
101
  /** Explicit pin > per-call cwd > client roots > launch cwd.
89
102
  * A global desktop selection must never redirect another CLI session. */
90
103
  function resolveRepo(args = {}) {
@@ -111,7 +124,9 @@ function resolveRepo(args = {}) {
111
124
  return top
112
125
  }))]
113
126
  if (dirs.length === 1) return dirs[0]
114
- const launched = gitRoot(process.cwd())
127
+ // ์‹œ์ž‘ ํด๋”๋Š” ์—ฌ๋Ÿฌ roots ์ค‘ ํ•˜๋‚˜๋ฅผ ๊ณ ๋ฅด๋Š” ๊ทผ๊ฑฐ๋กœ๋งŒ ์“ด๋‹ค. ํŒ์ •ํ•˜์ง€ ๋ชปํ•˜๋ฉด ๊ทผ๊ฑฐ๊ฐ€ ์—†๋Š” ๊ฒƒ์ด๋‹ค.
128
+ let launched = null
129
+ try { launched = gitRoot(process.cwd()) } catch { /* ๊ณ ๋ฅผ ๊ทผ๊ฑฐ ์—†์Œ โ†’ ์•„๋ž˜ ์˜ค๋ฅ˜ */ }
115
130
  if (dirs.includes(launched)) return launched
116
131
  throw new Error('์ž‘์—… ์ €์žฅ์†Œ๋ฅผ ํ•˜๋‚˜๋กœ ์ •ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋„๊ตฌ์˜ cwd ์— ์ž‘์—… ํด๋” ์ ˆ๋Œ€ ๊ฒฝ๋กœ๋ฅผ ์ง€์ •ํ•˜์„ธ์š”.')
117
132
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "//name": "๐Ÿ”ด npm ์˜ 'axmap' ์€ 2022๋…„๋ถ€ํ„ฐ ๋‚จ์ด ์“ฐ๊ณ  ์žˆ๋Š” ๋‹ค๋ฅธ ๊พธ๋Ÿฌ๋ฏธ๋‹ค(leozin_ ์˜ Map ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ, 1.1.2). ๊ทธ๋ž˜์„œ `npx axmap` ์€ ์šฐ๋ฆฌ ๊ฒƒ์ด ์•„๋‹ˆ๊ณ , ๋ˆ„๊ตฌ์—๊ฒŒ๋„ ๊ทธ๋ ‡๊ฒŒ ์•ˆ๋‚ดํ•˜๋ฉด ์•ˆ ๋œ๋‹ค. 2026-08-28 ์— axmap-cli ๋กœ ์ •ํ–ˆ๋‹ค โ€” ์Šค์ฝ”ํ”„(@์‚ฌ์šฉ์ž/์ด๋ฆ„)๋ฅผ ์“ฐ์ง€ ์•Š๋Š” ์ด์œ ๋Š” ์Šค์ฝ”ํ”„๊ฐ€ npm ๊ณ„์ • ์ด๋ฆ„์— ๋ฌถ์—ฌ์„œ ๋‚˜์ค‘์— ํŒ€์ด๋‚˜ ์กฐ์ง์œผ๋กœ ์˜ฎ๊ธธ ๋•Œ ์ด๋ฆ„์ด ํ†ต์งธ๋กœ ๋ฐ”๋€Œ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ์‚ฌ๋žŒ์ด ์น˜๋Š” **๋ช…๋ น** ์ด๋ฆ„์€ ๊ทธ๋Œ€๋กœ axmap ์ด๋‹ค (์•„๋ž˜ bin) โ€” ๊พธ๋Ÿฌ๋ฏธ ์ด๋ฆ„๊ณผ ๋ช…๋ น ์ด๋ฆ„์€ ๋‹ฌ๋ผ๋„ ๋œ๋‹ค.",
3
3
  "name": "axmap-cli",
4
- "version": "1.7.0",
4
+ "version": "1.9.0",
5
5
  "//private": "๐Ÿ”ด ์—ฌ๊ธฐ ์žˆ๋˜ \"private\": true ๋ฅผ 2026-08-28 ์— ์ง€์› ๋‹ค. ๊ทธ ์ค„์ด ์žˆ๋Š” ๋™์•ˆ npm publish ๋Š” ๊ฑฐ๋ถ€๋๋‹ค. ๋˜๋Œ๋ฆฌ๋ ค๋ฉด ๋‹ค์‹œ ๋„ฃ์œผ๋ฉด ๋˜์ง€๋งŒ, ์ด๋ฏธ ์˜ฌ๋ผ๊ฐ„ ๋ฒ„์ „์€ ๊ทธ๋ž˜๋„ ์•ˆ ์‚ฌ๋ผ์ง„๋‹ค โ€” npm ์€ ๊ฐ™์€ ๋ฒˆํ˜ธ๋ฅผ ๋ฎ์–ด์“ฐ์ง€ ๋ชปํ•˜๊ณ  ์‚ญ์ œ๋„ 72์‹œ๊ฐ„ ์•ˆ์—๋งŒ ๋œ๋‹ค. ์ฆ‰ ์ด ์ค„์„ ๋˜์‚ด๋ฆฌ๋Š” ๊ฒƒ์€ '์•ž์œผ๋กœ ์•ˆ ์˜ฌ๋ฆฐ๋‹ค' ๋Š” ๋œป์ด์ง€ '์˜ฌ๋ฆฐ ๊ฒƒ์„ ์—†์•ค๋‹ค' ๋Š” ๋œป์ด ์•„๋‹ˆ๋‹ค.",
6
6
  "//publishConfig": "axmap-cli ๋Š” ์Šค์ฝ”ํ”„๊ฐ€ ์—†์–ด์„œ ๊ธฐ๋ณธ์ด ์ด๋ฏธ ๊ณต๊ฐœ๋‹ค. ์ฆ‰ ์ง€๊ธˆ์€ ์—†์–ด๋„ ๋œ๋‹ค. ๊ทธ๋ž˜๋„ ๋‚จ๊ฒจ ๋‘๋Š” ์ด์œ : ๋‚˜์ค‘์— @์กฐ์ง/axmap ์ฒ˜๋Ÿผ ์Šค์ฝ”ํ”„๋ฅผ ๋ถ™์ด๋Š” ๋‚  ์ด ์ค„์ด ์—†์œผ๋ฉด publish ๊ฐ€ '์œ ๋ฃŒ ํ”Œ๋žœ์ด ํ•„์š”ํ•˜๋‹ค'๋Š” ์—‰๋šฑํ•œ ๋ง๋กœ ์‹คํŒจํ•œ๋‹ค.",
7
7
  "publishConfig": {
@@ -34,6 +34,7 @@
34
34
  "tools/",
35
35
  "skills/",
36
36
  "governance/",
37
+ "docs/VIEW.md",
37
38
  ".claude/",
38
39
  "CLAUDE.md"
39
40
  ],
@@ -0,0 +1,8 @@
1
+ ---
2
+ name: axmap-view
3
+ description: Show the current Git repository's axMap sessions, active claims, pending mailbox and recent events in a terminal view.
4
+ ---
5
+
6
+ Use the current Git repository or the user's explicitly designated repository. Run `axmap view --cwd <absolute-repository>` in an interactive terminal; `q`, Esc or Ctrl+C closes the view. If the host cannot provide an interactive terminal, run `axmap view --cwd <absolute-repository> --once` and identify it as a snapshot. Slash-menu availability depends on the host and installed skill; do not imply that a bare slash command works in every terminal.
7
+
8
+ The display reads local evidence only. It does not start an AI session, submit work, send or collect mail, or imply that an unobserved session is idle. Never invent progress percentages or token counts.
package/src/bridge.mjs CHANGED
@@ -9,7 +9,8 @@ import { spawnSync } from 'node:child_process';
9
9
  import { fileURLToPath } from 'node:url';
10
10
  import { withRepoLock } from './repolock.mjs';
11
11
  import { submitConsole, checkConsole, validateConsolePrompt, findConsoleTranscript, externalStatus } from './external-console.mjs';
12
- import { errorCode, errorMessage } from './session-types.mjs';
12
+ import { errorCode, errorMessage, RPC_NOT_DELIVERED_EXIT } from './session-types.mjs';
13
+ import { commonKey, resolveTarget } from './workspace.mjs';
13
14
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
14
15
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
15
16
  const bridgeHome = () => path.join(process.env.AXMAP_STATE_DIR || path.join(os.homedir(), '.axmap'), 'bridge');
@@ -45,14 +46,23 @@ function writeJSON(p, data) {
45
46
  fs.unlinkSync(tmp);
46
47
  }
47
48
  }
48
- function repoKey(cwd) {
49
+ function commonDirKey(cwd) {
49
50
  const r = spawnSync('git', ['rev-parse', '--path-format=absolute', '--git-common-dir'], {
50
51
  cwd, encoding: 'utf8', windowsHide: true,
51
52
  });
52
- if (r.status !== 0)
53
+ return r.status === 0 ? commonKey(fs.realpathSync(r.stdout.trim())) : null;
54
+ }
55
+ function repoKey(cwd) {
56
+ // Git ์•ˆ์ด๋ฉด ์ง€๊ธˆ๊นŒ์ง€์™€ ๊ฐ™์€ ํ•œ ๋ฒˆ์˜ git ํ˜ธ์ถœ๋กœ ๋๋‚ธ๋‹ค. ์‹คํŒจํ–ˆ์„ ๋•Œ๋งŒ ์ž‘์—… ํด๋”
57
+ // ๊ทœ์น™(์ˆ˜๋™ ์„ค์ • โ†’ ์ž์‹ ์ €์žฅ์†Œ ์ž๋™ ํƒ์ƒ‰ โ†’ ๋ชจํ˜ธํ•˜๋ฉด ์˜ค๋ฅ˜)์œผ๋กœ ๋Œ€์ƒ ์ €์žฅ์†Œ๋ฅผ ์ •ํ•œ๋‹ค.
58
+ const direct = commonDirKey(cwd);
59
+ if (direct)
60
+ return direct;
61
+ const repo = resolveTarget(cwd);
62
+ const key = repo && commonDirKey(repo);
63
+ if (!key)
53
64
  throw new Error(`Git ์ž‘์—… ์ €์žฅ์†Œ๋ฅผ ํ™•์ธํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค: ${cwd}`);
54
- const key = fs.realpathSync(r.stdout.trim());
55
- return process.platform === 'win32' ? key.toLowerCase() : key;
65
+ return key;
56
66
  }
57
67
  function addressError(address) {
58
68
  // Only a loopback address exported by a local provider hook is accepted.
@@ -98,20 +108,96 @@ function transcript(session) {
98
108
  lines.pop();
99
109
  return lines.filter(s => s.trim()).map(s => JSON.parse(s));
100
110
  }
101
- function publicSession(s) {
111
+ function publicSession(s, liveness) {
102
112
  return { id: s.id, provider: s.provider, workspaces: s.workspaces, model: s.model,
103
113
  conversationId: s.conversationId,
104
114
  transport: s.transport || 'agentapi', state: s.state, pid: s.pid,
105
- registeredAt: s.registeredAt, event: s.event, fullyIdle: s.fullyIdle,
106
- role: s.role, usage: s.usage, usageKnown: s.usageKnown, maxTokens: s.maxTokens, pendingApproval: s.pendingApproval };
115
+ registeredAt: s.registeredAt, event: s.event,
116
+ // ํ˜ธ์ŠคํŠธ๊ฐ€ ์‚ด์•„ ์žˆ๋Š”์ง€ ํ™•์ธํ•˜์ง€ ๋ชปํ–ˆ์œผ๋ฉด ์ €์žฅ๋œ "๋Œ€๊ธฐ"๋ฅผ ๋Œ€๊ธฐ๋กœ ๋ณด๊ณ ํ•˜์ง€ ์•Š๋Š”๋‹ค.
117
+ fullyIdle: liveness === 'unverified' ? false : s.fullyIdle,
118
+ role: s.role, usage: s.usage, usageKnown: s.usageKnown, maxTokens: s.maxTokens, pendingApproval: s.pendingApproval,
119
+ ...(liveness ? { liveness } : {}) };
120
+ }
121
+ /** ๊ด€๋ฆฌํ˜• ์„ธ์…˜(ptyยทstdio)์˜ ๋กœ์ปฌ ์ œ์–ด ํŒŒ์ดํ”„. ์„ธ์…˜ ํ˜ธ์ŠคํŠธ๊ฐ€ ์‚ด์•„ ์žˆ๋Š” ๋™์•ˆ๋งŒ ์กด์žฌํ•œ๋‹ค. */
122
+ export const hostPipe = (id) => process.platform === 'win32' ? `\\\\.\\pipe\\axmap-${id}` : path.join(bridgeHome(), `${id}.sock`);
123
+ /** Windows ์˜ ์—ด๋ฆฐ ํŒŒ์ดํ”„ ์ด๋ฆ„ ๋ชฉ๋ก. ์ฝ์ง€ ๋ชปํ•˜๋ฉด ์˜ค๋ฅ˜ ์ž์ฒด๋ฅผ ๋Œ๋ ค์ค€๋‹ค โ€”
124
+ * "๋ชฉ๋ก์— ์—†์Œ"๊ณผ "๋ชฉ๋ก์„ ๋ชป ์ฝ์Œ"์„ ๊ฐ™์€ ๊ฒƒ์œผ๋กœ ๋ณด๋ฉด ์‚ฐ ์„ธ์…˜์„ ๋‹ซํž˜์œผ๋กœ ์ ๋Š”๋‹ค. */
125
+ function openPipes() {
126
+ try {
127
+ return new Set(fs.readdirSync('\\\\.\\pipe\\').map(n => n.toLowerCase()));
128
+ }
129
+ catch (e) {
130
+ return e instanceof Error ? e : new Error(String(e));
131
+ }
132
+ }
133
+ /**
134
+ * ์ €์žฅ๋œ ์ƒํƒœ๊ฐ€ ์•„๋‹ˆ๋ผ ํ˜ธ์ŠคํŠธ๊ฐ€ ์‹ค์ œ๋กœ ์‚ด์•„ ์žˆ๋Š”์ง€. ์—ฐ๊ฒฐ ์š”์ฒญ์€ ๋ณด๋‚ด์ง€ ์•Š๋Š”๋‹ค.
135
+ *
136
+ * `bridge serve` ๊ฐ€ ๊ฐ•์ œ ์ข…๋ฃŒ๋˜๋ฉด ์ข…๋ฃŒ ์ฒ˜๋ฆฌ(state: closed)๋ฅผ ์“ฐ์ง€ ๋ชปํ•ด ๊ธฐ๋ก์ด
137
+ * ์˜์›ํžˆ `idle` ๋กœ ๋‚จ๋Š”๋‹ค (์‹ค์ธก: ์„ธ์…˜ d3cde187, ํŒŒ์ดํ”„ ์—†์Œ). ํŒŒ์ดํ”„๋Š” ํ˜ธ์ŠคํŠธ๊ฐ€
138
+ * listen ํ•œ ๋’ค์—๋งŒ ๊ธฐ๋ก์„ ์“ฐ๋ฏ€๋กœ, ํŒŒ์ดํ”„๊ฐ€ ์—†๋‹ค๋Š” ๊ฒƒ์€ ํ˜ธ์ŠคํŠธ๊ฐ€ ๋๋‚ฌ๋‹ค๋Š” ๋œป์ด๋‹ค.
139
+ * Windows : ํŒŒ์ดํ”„ ์ด๋ฆ„ ๋ชฉ๋ก์— ์—†์œผ๋ฉด dead, ๋ชฉ๋ก์„ ๋ชป ์ฝ์œผ๋ฉด unverified
140
+ * ๊ทธ ๋ฐ– : ์†Œ์ผ“ ํŒŒ์ผ์ด ์—†์œผ๋ฉด dead. ์žˆ์œผ๋ฉด ๊ธฐ๋ก๋œ ํ˜ธ์ŠคํŠธ pid ๋กœ ํŒ๋‹จํ•˜๊ณ ,
141
+ * pid ๊ฐ€ ์—†์œผ๋ฉด(์ด์ „ ๋ฒ„์ „ ๊ธฐ๋ก) unverified โ€” ๋‚จ์€ ์†Œ์ผ“ ํŒŒ์ผ๋งŒ์œผ๋กœ๋Š” ๋ชจ๋ฅธ๋‹ค.
142
+ */
143
+ function hostLiveness(s, pipes) {
144
+ if (process.platform === 'win32') {
145
+ const listed = pipes();
146
+ if (listed instanceof Error)
147
+ return 'unverified';
148
+ return listed.has(`axmap-${s.id}`.toLowerCase()) ? 'alive' : 'dead';
149
+ }
150
+ try {
151
+ fs.statSync(hostPipe(s.id));
152
+ }
153
+ catch (e) {
154
+ return errorCode(e) === 'ENOENT' ? 'dead' : 'unverified';
155
+ }
156
+ if (!Number.isSafeInteger(s.hostPid) || s.hostPid <= 0)
157
+ return 'unverified';
158
+ try {
159
+ process.kill(s.hostPid, 0);
160
+ return 'alive';
161
+ }
162
+ catch (e) {
163
+ return errorCode(e) === 'ESRCH' ? 'dead' : errorCode(e) === 'EPERM' ? 'alive' : 'unverified';
164
+ }
165
+ }
166
+ function markClosed(target, s) {
167
+ // ํ˜ธ์ŠคํŠธ๊ฐ€ ๋๋‚œ ๋’ค ๊ธฐ๋ก์„ ๋ฐ”๊พธ๋Š” ์ชฝ์€ ์—ฌ๊ธฐ๋ฟ์ด์ง€๋งŒ, ๋ง‰ ์ข…๋ฃŒ ์ฒ˜๋ฆฌ ์ค‘์ธ ํ˜ธ์ŠคํŠธ๊ฐ€ ๋”
168
+ // ์ตœ์‹  ๊ธฐ๋ก(์‚ฌ์šฉ๋Ÿ‰ ๋“ฑ)์„ ์ผ์„ ์ˆ˜ ์žˆ์–ด ๋‹ค์‹œ ์ฝ๊ณ  ๋ฐ”๊พผ๋‹ค.
169
+ let latest = s;
170
+ try {
171
+ latest = readJSON(target);
172
+ }
173
+ catch { /* ์•„๋ž˜์—์„œ ์ฝ์€ ๊ธฐ๋ก์œผ๋กœ ํ‘œ์‹œ๋งŒ ํ•œ๋‹ค */ }
174
+ if (latest.state === 'closed')
175
+ return latest;
176
+ const closed = { ...latest, state: 'closed', fullyIdle: false,
177
+ closedReason: '์„ธ์…˜ ํ˜ธ์ŠคํŠธ์˜ ๋กœ์ปฌ ์—ฐ๊ฒฐ ํŒŒ์ดํ”„๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ์ •์ƒ ์ข…๋ฃŒ ์ฒ˜๋ฆฌ ์—†์ด ๋๋‚œ ๊ฒƒ์œผ๋กœ ๋ณด์ž…๋‹ˆ๋‹ค.' };
178
+ // ์ €์žฅ์— ์‹คํŒจํ•ด๋„ ์ด๋ฒˆ ์กฐํšŒ์—๋Š” ๋‹ซํž˜์œผ๋กœ ๋ณด๊ณ ํ•œ๋‹ค. ๋‹ค์Œ ์กฐํšŒ๊ฐ€ ๋‹ค์‹œ ์‹œ๋„ํ•œ๋‹ค.
179
+ try {
180
+ writeJSON(target, closed);
181
+ }
182
+ catch { /* ์œ„์™€ ๊ฐ™๋‹ค */ }
183
+ return closed;
107
184
  }
108
185
  export function sessions(cwd) {
109
186
  const dir = path.join(bridgeHome(), 'sessions');
110
187
  if (!fs.existsSync(dir))
111
188
  return [];
112
189
  const repo = repoKey(cwd);
190
+ let pipes;
191
+ const listPipes = () => (pipes ??= openPipes());
113
192
  return fs.readdirSync(dir).filter(n => UUID.test(n.replace(/\.json$/, '')) && n.endsWith('.json'))
114
- .map(n => readJSON(path.join(dir, n))).filter(s => s.repos.includes(repo)).map(publicSession);
193
+ .map(n => [path.join(dir, n), readJSON(path.join(dir, n))])
194
+ .filter(([, s]) => s.repos.includes(repo))
195
+ .map(([target, s]) => {
196
+ if (!['pty', 'stdio'].includes(s.transport || '') || s.state === 'closed')
197
+ return publicSession(s);
198
+ const liveness = hostLiveness(s, listPipes);
199
+ return liveness === 'dead' ? publicSession(markClosed(target, s), 'dead') : publicSession(s, liveness);
200
+ });
115
201
  }
116
202
  function sessionFor(id, cwd) {
117
203
  const s = readJSON(file('sessions', id));
@@ -217,59 +303,85 @@ export function sessionLeasePath(id) {
217
303
  return path.join(bridgeHome(), 'dialogues', `session-${id}.lock`);
218
304
  }
219
305
  export function dispatchRequest({ to, prompt, cwd, from = 'axmap', dialogueId = null }) {
220
- if (typeof prompt !== 'string' || !prompt.trim() || Buffer.byteLength(prompt) > 32768)
221
- throw new Error('์š”์ฒญ ๋ณธ๋ฌธ์€ 1~32768๋ฐ”์ดํŠธ์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.');
222
- const s = sessionFor(to, cwd);
223
- const lease = sessionLeasePath(to);
224
- if (fs.existsSync(lease) && readJSON(lease).dialogueId !== dialogueId)
225
- throw new Error('์ด ์„ธ์…˜์€ ์ž๋™ ๋Œ€ํ™”๊ฐ€ ์‚ฌ์šฉ ์ค‘์ž…๋‹ˆ๋‹ค. ํ•ด๋‹น ๋Œ€ํ™”๊ฐ€ ๋๋‚œ ๋’ค ์š”์ฒญํ•˜์„ธ์š”.');
226
- const dir = path.join(bridgeHome(), 'requests');
227
- fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
228
- return withRepoLock(path.join(dir, `${to}.lock`), () => {
306
+ // ๋ณด๋‚ด๊ธฐ ์‹œ์ž‘ ์ „์— ๋‚œ ์˜ค๋ฅ˜(์ด์ „ ์š”์ฒญ ๋ฏธํ™•์ •, ๋Œ€๊ธฐ ๋ฏธํ™•์ธ ๋“ฑ)๋Š” ํ™•์‹คํžˆ ์ „๋‹ฌ๋˜์ง€ ์•Š์€ ๊ฒƒ์ด๋‹ค.
307
+ // ๋ฐ›์€ ํŽธ์ง€ํ•จ์ด ์ด๋ฅผ "์ „๋‹ฌ ๋ถˆ๋ช…"๊ณผ ๊ตฌ๋ณ„ํ•ด์•ผ ์ชฝ์ง€๋ฅผ ๋‹ค์‹œ ๋Œ€๊ธฐ์—ด๋กœ ๋Œ๋ฆด ์ˆ˜ ์žˆ๋‹ค.
308
+ let sending = false;
309
+ try {
310
+ if (typeof prompt !== 'string' || !prompt.trim() || Buffer.byteLength(prompt) > 32768)
311
+ throw new Error('์š”์ฒญ ๋ณธ๋ฌธ์€ 1~32768๋ฐ”์ดํŠธ์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.');
312
+ const s = sessionFor(to, cwd);
313
+ const lease = sessionLeasePath(to);
229
314
  if (fs.existsSync(lease) && readJSON(lease).dialogueId !== dialogueId)
230
- throw new Error('์ด ์„ธ์…˜์€ ์ž๋™ ๋Œ€ํ™”๊ฐ€ ์‚ฌ์šฉ ์ค‘์ž…๋‹ˆ๋‹ค.');
231
- for (const n of fs.readdirSync(dir).filter(n => n.endsWith('.json'))) {
232
- const previous = readJSON(path.join(dir, n));
233
- if (previous.to !== to || !['sending', 'submitted', 'unknown'].includes(previous.state))
234
- continue;
235
- const latest = previous.state === 'sending' ? previous : requestResult(previous.id, cwd);
236
- if (['sending', 'submitted', 'unknown'].includes(latest.state))
237
- throw new Error(`์ด ์„ธ์…˜์˜ ์ด์ „ ์š”์ฒญ์ด ์•„์ง ํ™•์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค: ${latest.id}`);
238
- }
239
- // No fallback to `agy -p` or --conversation: both would start a separate process.
240
- let consolePreflight;
241
- if (s.transport === 'console') {
242
- validateConsolePrompt(prompt, from);
243
- consolePreflight = checkConsole(s);
244
- }
245
- if (!['pty', 'stdio', 'console'].includes(s.transport || '')) {
246
- runAPI(s, ['get-conversation-metadata', to]);
247
- if (!s.fullyIdle || s.event !== 'Stop')
248
- throw new Error('์ƒ๋Œ€ ์„ธ์…˜์˜ ๋Œ€๊ธฐ ์ƒํƒœ๋ฅผ ์•„์ง ํ™•์ธํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค. ์ž‘์—… ์ข…๋ฃŒ ํ›…์„ ๊ธฐ๋‹ค๋ฆฌ์„ธ์š”.');
249
- }
250
- const request = { id: randomUUID(), to, from, repo: repoKey(cwd), prompt,
251
- createdAt: new Date().toISOString(), baseline: ['pty', 'stdio'].includes(s.transport || '') ? null : s.transcript ? transcript(s).length : 0, state: 'sending' };
252
- const target = file('requests', request.id);
253
- writeJSON(target, request);
254
- try {
255
- request.receipt = s.transport === 'console' ? submitConsole(s, request, consolePreflight) : ['pty', 'stdio'].includes(s.transport || '')
256
- ? terminalRPC(to, { action: 'submit', request })
257
- : runAPI(s, ['send-message', to, `[axMap request ${request.id}]\n์š”์ฒญ์ž: ${from}\n${prompt}`]);
258
- request.state = 'submitted';
259
- }
260
- catch (e) {
261
- // A timeout may occur after delivery. Never retry automatically.
262
- request.state = 'unknown';
263
- request.error = errorMessage(e);
264
- }
265
- writeJSON(target, request);
266
- return request;
267
- });
315
+ throw new Error('์ด ์„ธ์…˜์€ ์ž๋™ ๋Œ€ํ™”๊ฐ€ ์‚ฌ์šฉ ์ค‘์ž…๋‹ˆ๋‹ค. ํ•ด๋‹น ๋Œ€ํ™”๊ฐ€ ๋๋‚œ ๋’ค ์š”์ฒญํ•˜์„ธ์š”.');
316
+ const dir = path.join(bridgeHome(), 'requests');
317
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
318
+ return withRepoLock(path.join(dir, `${to}.lock`), () => {
319
+ if (fs.existsSync(lease) && readJSON(lease).dialogueId !== dialogueId)
320
+ throw new Error('์ด ์„ธ์…˜์€ ์ž๋™ ๋Œ€ํ™”๊ฐ€ ์‚ฌ์šฉ ์ค‘์ž…๋‹ˆ๋‹ค.');
321
+ for (const n of fs.readdirSync(dir).filter(n => n.endsWith('.json'))) {
322
+ const previous = readJSON(path.join(dir, n));
323
+ if (previous.to !== to || !['sending', 'submitted', 'unknown'].includes(previous.state))
324
+ continue;
325
+ const latest = previous.state === 'sending' ? previous : requestResult(previous.id, cwd);
326
+ if (['sending', 'submitted', 'unknown'].includes(latest.state))
327
+ throw new Error(`์ด ์„ธ์…˜์˜ ์ด์ „ ์š”์ฒญ์ด ์•„์ง ํ™•์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค: ${latest.id}`);
328
+ }
329
+ // No fallback to `agy -p` or --conversation: both would start a separate process.
330
+ let consolePreflight;
331
+ if (s.transport === 'console') {
332
+ validateConsolePrompt(prompt, from);
333
+ consolePreflight = checkConsole(s);
334
+ }
335
+ if (!['pty', 'stdio', 'console'].includes(s.transport || '')) {
336
+ runAPI(s, ['get-conversation-metadata', to]);
337
+ if (!s.fullyIdle || s.event !== 'Stop')
338
+ throw new Error('์ƒ๋Œ€ ์„ธ์…˜์˜ ๋Œ€๊ธฐ ์ƒํƒœ๋ฅผ ์•„์ง ํ™•์ธํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค. ์ž‘์—… ์ข…๋ฃŒ ํ›…์„ ๊ธฐ๋‹ค๋ฆฌ์„ธ์š”.');
339
+ }
340
+ const request = { id: randomUUID(), to, from, repo: repoKey(cwd), prompt,
341
+ createdAt: new Date().toISOString(), baseline: ['pty', 'stdio'].includes(s.transport || '') ? null : s.transcript ? transcript(s).length : 0, state: 'sending' };
342
+ const target = file('requests', request.id);
343
+ writeJSON(target, request);
344
+ sending = true;
345
+ try {
346
+ request.receipt = s.transport === 'console' ? submitConsole(s, request, consolePreflight) : ['pty', 'stdio'].includes(s.transport || '')
347
+ ? terminalRPC(to, { action: 'submit', request })
348
+ : runAPI(s, ['send-message', to, `[axMap request ${request.id}]\n์š”์ฒญ์ž: ${from}\n${prompt}`]);
349
+ request.state = 'submitted';
350
+ }
351
+ catch (e) {
352
+ if (notDelivered(e)) {
353
+ // ๋กœ์ปฌ ํŒŒ์ดํ”„์— ์—ฐ๊ฒฐ์กฐ์ฐจ ๋ชป ํ–ˆ๋‹ค(ENOENTยทECONNREFUSED). ํ•œ ๋ฐ”์ดํŠธ๋„ ๋ณด๋‚ด์ง€
354
+ // ์•Š์•˜์œผ๋ฏ€๋กœ ํ™•์‹คํžˆ ์ „๋‹ฌ๋˜์ง€ ์•Š์•˜๋‹ค. unknown ์œผ๋กœ ๋‘๋ฉด ์ด ์„ธ์…˜์˜ ๋‹ค์Œ ์š”์ฒญ์ด
355
+ // ์ „๋ถ€ ๋ง‰ํžŒ๋‹ค. ์ž๋™ ์žฌ์ „์†ก์€ ์—ฌ์ „ํžˆ ํ•˜์ง€ ์•Š๋Š”๋‹ค.
356
+ request.state = 'failed';
357
+ request.error = `์ „๋‹ฌ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค: ${errorMessage(e)}`;
358
+ }
359
+ else {
360
+ // A timeout may occur after delivery. Never retry automatically.
361
+ request.state = 'unknown';
362
+ request.error = errorMessage(e);
363
+ }
364
+ }
365
+ writeJSON(target, request);
366
+ return request;
367
+ });
368
+ }
369
+ catch (e) {
370
+ if (!sending && e instanceof Error)
371
+ Object.assign(e, { notDelivered: true });
372
+ throw e;
373
+ }
268
374
  }
375
+ /** ์—ฐ๊ฒฐ ์ „์— ์‹คํŒจํ•ด ์•„๋ฌด๊ฒƒ๋„ ๋ณด๋‚ด์ง€ ์•Š์€ ์˜ค๋ฅ˜๋งŒ true. ๋ฌธ๊ตฌ๊ฐ€ ์•„๋‹ˆ๋ผ ์ข…๋ฃŒ ์ฝ”๋“œ๋‚˜ ๋ณด๋‚ด๊ธฐ ์ „ ๋‹จ๊ณ„๋กœ ๊ฐ€๋ฅธ๋‹ค. */
376
+ export const notDelivered = (e) => e instanceof Error && e.notDelivered === true;
269
377
  function terminalRPC(id, message) {
270
378
  const r = spawnSync(process.execPath, [path.join(ROOT, 'tools', 'run.mjs'), 'rpc', id], {
271
379
  input: JSON.stringify(message), encoding: 'utf8', windowsHide: true, timeout: 15000,
272
380
  });
381
+ // ์‹œ๊ฐ„ ์ดˆ๊ณผ(r.error)๋‚˜ ๊ทธ ๋ฐ–์˜ ์‹คํŒจ๋Š” ์—ฐ๊ฒฐ ๋’ค์— ๋‚ฌ์„ ์ˆ˜ ์žˆ์–ด ์ „๋‹ฌ ์—ฌ๋ถ€๋ฅผ ๋ชจ๋ฅธ๋‹ค.
382
+ if (!r.error && r.status === RPC_NOT_DELIVERED_EXIT) {
383
+ throw Object.assign(new Error(r.stderr?.trim() || '๋กœ์ปฌ ์„ธ์…˜ ํŒŒ์ดํ”„์— ์—ฐ๊ฒฐํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.'), { notDelivered: true });
384
+ }
273
385
  if (r.error || r.status !== 0)
274
386
  throw new Error(r.error?.message || r.stderr?.trim() || 'ํ„ฐ๋ฏธ๋„ ์„ธ์…˜์— ์—ฐ๊ฒฐํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.');
275
387
  return JSON.parse(r.stdout);
package/src/bridge.mts CHANGED
@@ -8,7 +8,9 @@ import { spawnSync } from 'node:child_process'
8
8
  import { fileURLToPath } from 'node:url'
9
9
  import { withRepoLock } from './repolock.mjs'
10
10
  import { submitConsole, checkConsole, validateConsolePrompt, findConsoleTranscript, externalStatus } from './external-console.mjs'
11
- import { errorCode, errorMessage } from './session-types.mjs'
11
+ import { errorCode, errorMessage, RPC_NOT_DELIVERED_EXIT } from './session-types.mjs'
12
+ import { commonKey, resolveTarget } from './workspace.mjs'
13
+ import type { HostLiveness } from './session-types.mjs'
12
14
  import type { SessionRecord, PublicSession, RequestRecord, DispatchOptions, HookInput, TranscriptEntry } from './session-types.mjs'
13
15
 
14
16
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
@@ -37,13 +39,21 @@ function writeJSON(p: string, data: unknown): void {
37
39
  }
38
40
  } finally { if (fs.existsSync(tmp)) fs.unlinkSync(tmp) }
39
41
  }
40
- function repoKey(cwd: string): string {
42
+ function commonDirKey(cwd: string): string | null {
41
43
  const r = spawnSync('git', ['rev-parse', '--path-format=absolute', '--git-common-dir'], {
42
44
  cwd, encoding: 'utf8', windowsHide: true,
43
45
  })
44
- if (r.status !== 0) throw new Error(`Git ์ž‘์—… ์ €์žฅ์†Œ๋ฅผ ํ™•์ธํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค: ${cwd}`)
45
- const key = fs.realpathSync(r.stdout.trim())
46
- return process.platform === 'win32' ? key.toLowerCase() : key
46
+ return r.status === 0 ? commonKey(fs.realpathSync(r.stdout.trim())) : null
47
+ }
48
+ function repoKey(cwd: string): string {
49
+ // Git ์•ˆ์ด๋ฉด ์ง€๊ธˆ๊นŒ์ง€์™€ ๊ฐ™์€ ํ•œ ๋ฒˆ์˜ git ํ˜ธ์ถœ๋กœ ๋๋‚ธ๋‹ค. ์‹คํŒจํ–ˆ์„ ๋•Œ๋งŒ ์ž‘์—… ํด๋”
50
+ // ๊ทœ์น™(์ˆ˜๋™ ์„ค์ • โ†’ ์ž์‹ ์ €์žฅ์†Œ ์ž๋™ ํƒ์ƒ‰ โ†’ ๋ชจํ˜ธํ•˜๋ฉด ์˜ค๋ฅ˜)์œผ๋กœ ๋Œ€์ƒ ์ €์žฅ์†Œ๋ฅผ ์ •ํ•œ๋‹ค.
51
+ const direct = commonDirKey(cwd)
52
+ if (direct) return direct
53
+ const repo = resolveTarget(cwd)
54
+ const key = repo && commonDirKey(repo)
55
+ if (!key) throw new Error(`Git ์ž‘์—… ์ €์žฅ์†Œ๋ฅผ ํ™•์ธํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค: ${cwd}`)
56
+ return key
47
57
  }
48
58
  function addressError(address: unknown): asserts address is string {
49
59
  // Only a loopback address exported by a local provider hook is accepted.
@@ -79,19 +89,77 @@ function transcript(session: SessionRecord): TranscriptEntry[] {
79
89
  lines.pop()
80
90
  return lines.filter(s => s.trim()).map(s => JSON.parse(s) as TranscriptEntry)
81
91
  }
82
- function publicSession(s: SessionRecord): PublicSession {
92
+ function publicSession(s: SessionRecord, liveness?: HostLiveness): PublicSession {
83
93
  return { id: s.id, provider: s.provider, workspaces: s.workspaces, model: s.model,
84
94
  conversationId: s.conversationId,
85
95
  transport: s.transport || 'agentapi', state: s.state, pid: s.pid,
86
- registeredAt: s.registeredAt, event: s.event, fullyIdle: s.fullyIdle,
87
- role: s.role, usage: s.usage, usageKnown: s.usageKnown, maxTokens: s.maxTokens, pendingApproval: s.pendingApproval }
96
+ registeredAt: s.registeredAt, event: s.event,
97
+ // ํ˜ธ์ŠคํŠธ๊ฐ€ ์‚ด์•„ ์žˆ๋Š”์ง€ ํ™•์ธํ•˜์ง€ ๋ชปํ–ˆ์œผ๋ฉด ์ €์žฅ๋œ "๋Œ€๊ธฐ"๋ฅผ ๋Œ€๊ธฐ๋กœ ๋ณด๊ณ ํ•˜์ง€ ์•Š๋Š”๋‹ค.
98
+ fullyIdle: liveness === 'unverified' ? false : s.fullyIdle,
99
+ role: s.role, usage: s.usage, usageKnown: s.usageKnown, maxTokens: s.maxTokens, pendingApproval: s.pendingApproval,
100
+ ...(liveness ? { liveness } : {}) }
101
+ }
102
+
103
+ /** ๊ด€๋ฆฌํ˜• ์„ธ์…˜(ptyยทstdio)์˜ ๋กœ์ปฌ ์ œ์–ด ํŒŒ์ดํ”„. ์„ธ์…˜ ํ˜ธ์ŠคํŠธ๊ฐ€ ์‚ด์•„ ์žˆ๋Š” ๋™์•ˆ๋งŒ ์กด์žฌํ•œ๋‹ค. */
104
+ export const hostPipe = (id: string) => process.platform === 'win32' ? `\\\\.\\pipe\\axmap-${id}` : path.join(bridgeHome(), `${id}.sock`)
105
+
106
+ /** Windows ์˜ ์—ด๋ฆฐ ํŒŒ์ดํ”„ ์ด๋ฆ„ ๋ชฉ๋ก. ์ฝ์ง€ ๋ชปํ•˜๋ฉด ์˜ค๋ฅ˜ ์ž์ฒด๋ฅผ ๋Œ๋ ค์ค€๋‹ค โ€”
107
+ * "๋ชฉ๋ก์— ์—†์Œ"๊ณผ "๋ชฉ๋ก์„ ๋ชป ์ฝ์Œ"์„ ๊ฐ™์€ ๊ฒƒ์œผ๋กœ ๋ณด๋ฉด ์‚ฐ ์„ธ์…˜์„ ๋‹ซํž˜์œผ๋กœ ์ ๋Š”๋‹ค. */
108
+ function openPipes(): Set<string> | Error {
109
+ try { return new Set(fs.readdirSync('\\\\.\\pipe\\').map(n => n.toLowerCase())) }
110
+ catch (e) { return e instanceof Error ? e : new Error(String(e)) }
88
111
  }
112
+
113
+ /**
114
+ * ์ €์žฅ๋œ ์ƒํƒœ๊ฐ€ ์•„๋‹ˆ๋ผ ํ˜ธ์ŠคํŠธ๊ฐ€ ์‹ค์ œ๋กœ ์‚ด์•„ ์žˆ๋Š”์ง€. ์—ฐ๊ฒฐ ์š”์ฒญ์€ ๋ณด๋‚ด์ง€ ์•Š๋Š”๋‹ค.
115
+ *
116
+ * `bridge serve` ๊ฐ€ ๊ฐ•์ œ ์ข…๋ฃŒ๋˜๋ฉด ์ข…๋ฃŒ ์ฒ˜๋ฆฌ(state: closed)๋ฅผ ์“ฐ์ง€ ๋ชปํ•ด ๊ธฐ๋ก์ด
117
+ * ์˜์›ํžˆ `idle` ๋กœ ๋‚จ๋Š”๋‹ค (์‹ค์ธก: ์„ธ์…˜ d3cde187, ํŒŒ์ดํ”„ ์—†์Œ). ํŒŒ์ดํ”„๋Š” ํ˜ธ์ŠคํŠธ๊ฐ€
118
+ * listen ํ•œ ๋’ค์—๋งŒ ๊ธฐ๋ก์„ ์“ฐ๋ฏ€๋กœ, ํŒŒ์ดํ”„๊ฐ€ ์—†๋‹ค๋Š” ๊ฒƒ์€ ํ˜ธ์ŠคํŠธ๊ฐ€ ๋๋‚ฌ๋‹ค๋Š” ๋œป์ด๋‹ค.
119
+ * Windows : ํŒŒ์ดํ”„ ์ด๋ฆ„ ๋ชฉ๋ก์— ์—†์œผ๋ฉด dead, ๋ชฉ๋ก์„ ๋ชป ์ฝ์œผ๋ฉด unverified
120
+ * ๊ทธ ๋ฐ– : ์†Œ์ผ“ ํŒŒ์ผ์ด ์—†์œผ๋ฉด dead. ์žˆ์œผ๋ฉด ๊ธฐ๋ก๋œ ํ˜ธ์ŠคํŠธ pid ๋กœ ํŒ๋‹จํ•˜๊ณ ,
121
+ * pid ๊ฐ€ ์—†์œผ๋ฉด(์ด์ „ ๋ฒ„์ „ ๊ธฐ๋ก) unverified โ€” ๋‚จ์€ ์†Œ์ผ“ ํŒŒ์ผ๋งŒ์œผ๋กœ๋Š” ๋ชจ๋ฅธ๋‹ค.
122
+ */
123
+ function hostLiveness(s: SessionRecord, pipes: () => Set<string> | Error): HostLiveness {
124
+ if (process.platform === 'win32') {
125
+ const listed = pipes()
126
+ if (listed instanceof Error) return 'unverified'
127
+ return listed.has(`axmap-${s.id}`.toLowerCase()) ? 'alive' : 'dead'
128
+ }
129
+ try { fs.statSync(hostPipe(s.id)) } catch (e) { return errorCode(e) === 'ENOENT' ? 'dead' : 'unverified' }
130
+ if (!Number.isSafeInteger(s.hostPid) || (s.hostPid as number) <= 0) return 'unverified'
131
+ try { process.kill(s.hostPid as number, 0); return 'alive' } catch (e) {
132
+ return errorCode(e) === 'ESRCH' ? 'dead' : errorCode(e) === 'EPERM' ? 'alive' : 'unverified'
133
+ }
134
+ }
135
+
136
+ function markClosed(target: string, s: SessionRecord): SessionRecord {
137
+ // ํ˜ธ์ŠคํŠธ๊ฐ€ ๋๋‚œ ๋’ค ๊ธฐ๋ก์„ ๋ฐ”๊พธ๋Š” ์ชฝ์€ ์—ฌ๊ธฐ๋ฟ์ด์ง€๋งŒ, ๋ง‰ ์ข…๋ฃŒ ์ฒ˜๋ฆฌ ์ค‘์ธ ํ˜ธ์ŠคํŠธ๊ฐ€ ๋”
138
+ // ์ตœ์‹  ๊ธฐ๋ก(์‚ฌ์šฉ๋Ÿ‰ ๋“ฑ)์„ ์ผ์„ ์ˆ˜ ์žˆ์–ด ๋‹ค์‹œ ์ฝ๊ณ  ๋ฐ”๊พผ๋‹ค.
139
+ let latest = s
140
+ try { latest = readJSON<SessionRecord>(target) } catch { /* ์•„๋ž˜์—์„œ ์ฝ์€ ๊ธฐ๋ก์œผ๋กœ ํ‘œ์‹œ๋งŒ ํ•œ๋‹ค */ }
141
+ if (latest.state === 'closed') return latest
142
+ const closed: SessionRecord = { ...latest, state: 'closed', fullyIdle: false,
143
+ closedReason: '์„ธ์…˜ ํ˜ธ์ŠคํŠธ์˜ ๋กœ์ปฌ ์—ฐ๊ฒฐ ํŒŒ์ดํ”„๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ์ •์ƒ ์ข…๋ฃŒ ์ฒ˜๋ฆฌ ์—†์ด ๋๋‚œ ๊ฒƒ์œผ๋กœ ๋ณด์ž…๋‹ˆ๋‹ค.' }
144
+ // ์ €์žฅ์— ์‹คํŒจํ•ด๋„ ์ด๋ฒˆ ์กฐํšŒ์—๋Š” ๋‹ซํž˜์œผ๋กœ ๋ณด๊ณ ํ•œ๋‹ค. ๋‹ค์Œ ์กฐํšŒ๊ฐ€ ๋‹ค์‹œ ์‹œ๋„ํ•œ๋‹ค.
145
+ try { writeJSON(target, closed) } catch { /* ์œ„์™€ ๊ฐ™๋‹ค */ }
146
+ return closed
147
+ }
148
+
89
149
  export function sessions(cwd: string): PublicSession[] {
90
150
  const dir = path.join(bridgeHome(), 'sessions')
91
151
  if (!fs.existsSync(dir)) return []
92
152
  const repo = repoKey(cwd)
153
+ let pipes: Set<string> | Error | undefined
154
+ const listPipes = () => (pipes ??= openPipes())
93
155
  return fs.readdirSync(dir).filter(n => UUID.test(n.replace(/\.json$/, '')) && n.endsWith('.json'))
94
- .map(n => readJSON<SessionRecord>(path.join(dir, n))).filter(s => s.repos.includes(repo)).map(publicSession)
156
+ .map(n => [path.join(dir, n), readJSON<SessionRecord>(path.join(dir, n))] as const)
157
+ .filter(([, s]) => s.repos.includes(repo))
158
+ .map(([target, s]) => {
159
+ if (!['pty', 'stdio'].includes(s.transport || '') || s.state === 'closed') return publicSession(s)
160
+ const liveness = hostLiveness(s, listPipes)
161
+ return liveness === 'dead' ? publicSession(markClosed(target, s), 'dead') : publicSession(s, liveness)
162
+ })
95
163
  }
96
164
  function sessionFor(id: string, cwd: string): SessionRecord {
97
165
  const s = readJSON<SessionRecord>(file('sessions', id))
@@ -180,6 +248,10 @@ export function sessionLeasePath(id: string): string {
180
248
  }
181
249
 
182
250
  export function dispatchRequest({ to, prompt, cwd, from = 'axmap', dialogueId = null }: DispatchOptions): RequestRecord {
251
+ // ๋ณด๋‚ด๊ธฐ ์‹œ์ž‘ ์ „์— ๋‚œ ์˜ค๋ฅ˜(์ด์ „ ์š”์ฒญ ๋ฏธํ™•์ •, ๋Œ€๊ธฐ ๋ฏธํ™•์ธ ๋“ฑ)๋Š” ํ™•์‹คํžˆ ์ „๋‹ฌ๋˜์ง€ ์•Š์€ ๊ฒƒ์ด๋‹ค.
252
+ // ๋ฐ›์€ ํŽธ์ง€ํ•จ์ด ์ด๋ฅผ "์ „๋‹ฌ ๋ถˆ๋ช…"๊ณผ ๊ตฌ๋ณ„ํ•ด์•ผ ์ชฝ์ง€๋ฅผ ๋‹ค์‹œ ๋Œ€๊ธฐ์—ด๋กœ ๋Œ๋ฆด ์ˆ˜ ์žˆ๋‹ค.
253
+ let sending = false
254
+ try {
183
255
  if (typeof prompt !== 'string' || !prompt.trim() || Buffer.byteLength(prompt) > 32768) throw new Error('์š”์ฒญ ๋ณธ๋ฌธ์€ 1~32768๋ฐ”์ดํŠธ์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.')
184
256
  const s = sessionFor(to, cwd)
185
257
  const lease = sessionLeasePath(to)
@@ -205,25 +277,45 @@ export function dispatchRequest({ to, prompt, cwd, from = 'axmap', dialogueId =
205
277
  createdAt: new Date().toISOString(), baseline: ['pty', 'stdio'].includes(s.transport || '') ? null : s.transcript ? transcript(s).length : 0, state: 'sending' }
206
278
  const target = file('requests', request.id)
207
279
  writeJSON(target, request)
280
+ sending = true
208
281
  try {
209
282
  request.receipt = s.transport === 'console' ? submitConsole(s, request, consolePreflight) : ['pty', 'stdio'].includes(s.transport || '')
210
283
  ? terminalRPC(to, { action: 'submit', request })
211
284
  : runAPI(s, ['send-message', to, `[axMap request ${request.id}]\n์š”์ฒญ์ž: ${from}\n${prompt}`])
212
285
  request.state = 'submitted'
213
286
  } catch (e) {
214
- // A timeout may occur after delivery. Never retry automatically.
215
- request.state = 'unknown'
216
- request.error = errorMessage(e)
287
+ if (notDelivered(e)) {
288
+ // ๋กœ์ปฌ ํŒŒ์ดํ”„์— ์—ฐ๊ฒฐ์กฐ์ฐจ ๋ชป ํ–ˆ๋‹ค(ENOENTยทECONNREFUSED). ํ•œ ๋ฐ”์ดํŠธ๋„ ๋ณด๋‚ด์ง€
289
+ // ์•Š์•˜์œผ๋ฏ€๋กœ ํ™•์‹คํžˆ ์ „๋‹ฌ๋˜์ง€ ์•Š์•˜๋‹ค. unknown ์œผ๋กœ ๋‘๋ฉด ์ด ์„ธ์…˜์˜ ๋‹ค์Œ ์š”์ฒญ์ด
290
+ // ์ „๋ถ€ ๋ง‰ํžŒ๋‹ค. ์ž๋™ ์žฌ์ „์†ก์€ ์—ฌ์ „ํžˆ ํ•˜์ง€ ์•Š๋Š”๋‹ค.
291
+ request.state = 'failed'
292
+ request.error = `์ „๋‹ฌ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค: ${errorMessage(e)}`
293
+ } else {
294
+ // A timeout may occur after delivery. Never retry automatically.
295
+ request.state = 'unknown'
296
+ request.error = errorMessage(e)
297
+ }
217
298
  }
218
299
  writeJSON(target, request)
219
300
  return request
220
301
  })
302
+ } catch (e) {
303
+ if (!sending && e instanceof Error) Object.assign(e, { notDelivered: true })
304
+ throw e
305
+ }
221
306
  }
222
307
 
308
+ /** ์—ฐ๊ฒฐ ์ „์— ์‹คํŒจํ•ด ์•„๋ฌด๊ฒƒ๋„ ๋ณด๋‚ด์ง€ ์•Š์€ ์˜ค๋ฅ˜๋งŒ true. ๋ฌธ๊ตฌ๊ฐ€ ์•„๋‹ˆ๋ผ ์ข…๋ฃŒ ์ฝ”๋“œ๋‚˜ ๋ณด๋‚ด๊ธฐ ์ „ ๋‹จ๊ณ„๋กœ ๊ฐ€๋ฅธ๋‹ค. */
309
+ export const notDelivered =(e: unknown) => e instanceof Error && (e as Error & { notDelivered?: boolean }).notDelivered === true
310
+
223
311
  function terminalRPC(id: string, message: unknown): unknown {
224
312
  const r = spawnSync(process.execPath, [path.join(ROOT, 'tools', 'run.mjs'), 'rpc', id], {
225
313
  input: JSON.stringify(message), encoding: 'utf8', windowsHide: true, timeout: 15000,
226
314
  })
315
+ // ์‹œ๊ฐ„ ์ดˆ๊ณผ(r.error)๋‚˜ ๊ทธ ๋ฐ–์˜ ์‹คํŒจ๋Š” ์—ฐ๊ฒฐ ๋’ค์— ๋‚ฌ์„ ์ˆ˜ ์žˆ์–ด ์ „๋‹ฌ ์—ฌ๋ถ€๋ฅผ ๋ชจ๋ฅธ๋‹ค.
316
+ if (!r.error && r.status === RPC_NOT_DELIVERED_EXIT) {
317
+ throw Object.assign(new Error(r.stderr?.trim() || '๋กœ์ปฌ ์„ธ์…˜ ํŒŒ์ดํ”„์— ์—ฐ๊ฒฐํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.'), { notDelivered: true })
318
+ }
227
319
  if (r.error || r.status !== 0) throw new Error(r.error?.message || r.stderr?.trim() || 'ํ„ฐ๋ฏธ๋„ ์„ธ์…˜์— ์—ฐ๊ฒฐํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.')
228
320
  return JSON.parse(r.stdout)
229
321
  }
@@ -6,11 +6,12 @@ import path from 'node:path';
6
6
  import net from 'node:net';
7
7
  import { spawn } from 'node:child_process';
8
8
  import { randomUUID, randomBytes, timingSafeEqual } from 'node:crypto';
9
- import { bridgeHome, repoKey, writeJSON } from './bridge.mjs';
9
+ import { bridgeHome, hostPipe, repoKey, writeJSON } from './bridge.mjs';
10
10
  import { normalizeUsage, usageDelta } from './session-usage.mjs';
11
11
  import { errorMessage } from './session-types.mjs';
12
12
  import { connectCodexProxy } from './codex-connection.mjs';
13
- const pipeFor = (id) => process.platform === 'win32' ? `\\\\.\\pipe\\axmap-${id}` : path.join(bridgeHome(), `${id}.sock`);
13
+ // ์ƒ์กด ํ™•์ธ(bridge.sessions)์ด ๊ฐ™์€ ์ด๋ฆ„์„ ๋ณด๋„๋ก ํ•œ ๊ณณ์—์„œ ๋งŒ๋“ ๋‹ค.
14
+ const pipeFor = hostPipe;
14
15
  const uuid = (value) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value || '');
15
16
  export async function startManagedSession({ provider, executable = provider, prefix = [], args = null, model, role = '', existingThreadId, socketPath, observeActive = false, cwd = process.cwd(), maxTokens = 100000, timeoutMs = 120000, onEvent = () => { } }) {
16
17
  if (!['codex', 'agy'].includes(provider))
@@ -24,7 +25,7 @@ export async function startManagedSession({ provider, executable = provider, pre
24
25
  const id = randomUUID(), token = randomBytes(32).toString('hex');
25
26
  const sessionPath = path.join(bridgeHome(), 'sessions', `${id}.json`);
26
27
  const s = { id, token, provider, transport: 'stdio', role, model: model || null,
27
- workspaces: [path.resolve(cwd)], repos: [repoKey(cwd)], state: 'starting', fullyIdle: false,
28
+ workspaces: [path.resolve(cwd)], repos: [repoKey(cwd)], state: 'starting', fullyIdle: false, hostPid: process.pid,
28
29
  registeredAt: new Date().toISOString(), usage: null, usageKnown: true, maxTokens };
29
30
  const jobs = new Map();
30
31
  let attachedUsageOrigin = null;
@@ -5,7 +5,7 @@ import path from 'node:path'
5
5
  import net from 'node:net'
6
6
  import { spawn } from 'node:child_process'
7
7
  import { randomUUID, randomBytes, timingSafeEqual } from 'node:crypto'
8
- import { bridgeHome, repoKey, writeJSON } from './bridge.mjs'
8
+ import { bridgeHome, hostPipe, repoKey, writeJSON } from './bridge.mjs'
9
9
  import { normalizeUsage, usageDelta } from './session-usage.mjs'
10
10
  import { errorMessage } from './session-types.mjs'
11
11
  import { connectCodexProxy } from './codex-connection.mjs'
@@ -31,7 +31,8 @@ interface ProviderMessage {
31
31
  }
32
32
  interface AgyResult { conversation_id: string; status: string; response?: unknown; error?: string; usage?: unknown }
33
33
 
34
- const pipeFor = (id: string) => process.platform === 'win32' ? `\\\\.\\pipe\\axmap-${id}` : path.join(bridgeHome(), `${id}.sock`)
34
+ // ์ƒ์กด ํ™•์ธ(bridge.sessions)์ด ๊ฐ™์€ ์ด๋ฆ„์„ ๋ณด๋„๋ก ํ•œ ๊ณณ์—์„œ ๋งŒ๋“ ๋‹ค.
35
+ const pipeFor = hostPipe
35
36
  const uuid = (value: string | undefined) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value || '')
36
37
 
37
38
  export async function startManagedSession({ provider, executable = provider, prefix = [], args = null, model, role = '', existingThreadId, socketPath, observeActive = false,
@@ -43,7 +44,7 @@ export async function startManagedSession({ provider, executable = provider, pre
43
44
  const id = randomUUID(), token = randomBytes(32).toString('hex')
44
45
  const sessionPath = path.join(bridgeHome(), 'sessions', `${id}.json`)
45
46
  const s: SessionRecord = { id, token, provider, transport: 'stdio', role, model: model || null,
46
- workspaces: [path.resolve(cwd)], repos: [repoKey(cwd)], state: 'starting', fullyIdle: false,
47
+ workspaces: [path.resolve(cwd)], repos: [repoKey(cwd)], state: 'starting', fullyIdle: false, hostPid: process.pid,
47
48
  registeredAt: new Date().toISOString(), usage: null, usageKnown: true, maxTokens }
48
49
  const jobs = new Map<string, JobRecord>()
49
50
  let attachedUsageOrigin: Usage | null = null
@@ -3,12 +3,13 @@ import path from 'node:path';
3
3
  import { createMonitor, renderMonitor } from './monitor.mjs';
4
4
  import { errorMessage } from './session-types.mjs';
5
5
  import { renderPipelineGraph } from './pipeline-graph.mjs';
6
+ import { createView, renderView } from './view.mjs';
6
7
  export async function runMonitor(args) {
7
8
  if (args.includes('--help') || args.includes('-h')) {
8
9
  console.log('axmap monitor [--cwd <repo>] [--interval 1000] [--once | --json] [--no-color]\n์„ธ์…˜ยท์š”์ฒญ ํŒŒ์ดํ”„๋ผ์ธยท์ž๋™ ๋Œ€ํ™”ยท์„ ์ ยท์ตœ๊ทผ ์ด๋ฒคํŠธ๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค.\nTTY์—์„œ๋Š” ๊ณ„์† ๊ฐฑ์‹ ํ•˜๋ฉฐ, ํŒŒ์ดํ”„/๋ฆฌ๋””๋ ‰์…˜์—์„œ๋Š” ํ•œ ๋ฒˆ๋งŒ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค. q / Esc / Ctrl+C ์ข…๋ฃŒ.');
9
10
  return;
10
11
  }
11
- const allowed = new Set(['--cwd', '--interval', '--once', '--json', '--no-color', '--graph']);
12
+ const allowed = new Set(['--cwd', '--interval', '--once', '--json', '--no-color', '--graph', '--view']);
12
13
  for (let i = 0; i < args.length; i++) {
13
14
  if (!allowed.has(args[i]))
14
15
  throw new Error(`์•Œ ์ˆ˜ ์—†๋Š” ์˜ต์…˜: ${args[i]}`);
@@ -22,8 +23,9 @@ export async function runMonitor(args) {
22
23
  const interval = Number(option('--interval') || 1000);
23
24
  if (!Number.isInteger(interval) || interval < 250 || interval > 60000)
24
25
  throw new Error('interval์€ 250~60000ms ์ •์ˆ˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.');
25
- const collect = createMonitor(path.resolve(option('--cwd') || process.cwd()));
26
- const render = args.includes('--graph') ? renderPipelineGraph : renderMonitor;
26
+ const cwd = path.resolve(option('--cwd') || process.cwd());
27
+ const collect = args.includes('--view') ? createView(cwd) : createMonitor(cwd);
28
+ const render = args.includes('--view') ? renderView : args.includes('--graph') ? renderPipelineGraph : renderMonitor;
27
29
  const live = !!process.stdout.isTTY && !!process.stdin.isTTY && process.env.TERM !== 'dumb' && !args.includes('--once') && !args.includes('--json');
28
30
  const color = !!process.stdout.isTTY && !args.includes('--no-color') && process.env.NO_COLOR === undefined && process.env.TERM !== 'dumb';
29
31
  if (!live) {