agentosity 0.2.4 → 0.3.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ethan Yang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agentosity",
3
- "version": "0.2.4",
4
- "description": "Agentosity AI-native is a number now. Automatic attendance for your AI agents (stdio MCP lifecycle + activity probes) + human clock-out CLI.",
3
+ "version": "0.3.0",
4
+ "description": "Agentosity \u2014 AI-native is a number now. Automatic attendance for your AI agents (stdio MCP lifecycle + activity probes) + human clock-out CLI.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "agentosity": "./bin/agentosity.js"
@@ -28,4 +28,4 @@
28
28
  "directory": "packages/cli"
29
29
  },
30
30
  "license": "MIT"
31
- }
31
+ }
package/src/probes.js CHANGED
@@ -20,6 +20,13 @@ export function detectHarness(clientInfo) {
20
20
  if (n.includes("cursor")) return "cursor";
21
21
  if (n.includes("cline")) return "cline";
22
22
  if (n.includes("opencode")) return "opencode";
23
+ if (n.includes("kimi")) return "kimi";
24
+ if (n.includes("goose")) return "goose";
25
+ if (n.includes("hermes")) return "hermes";
26
+ if (n.includes("openclaw")) return "openclaw";
27
+ if (n.includes("grok")) return "grok";
28
+ if (n.includes("mimo")) return "mimo";
29
+ if (n === "pi" || n.includes("pi-agent") || n.includes("pi agent")) return "pi";
23
30
  return n || "unknown";
24
31
  }
25
32
 
@@ -50,18 +57,82 @@ function geminiSessionDir() {
50
57
  * 各 harness 的探针配置:
51
58
  * - dir:目录内找"我们这个会话"的文件(新建或最近在写的)
52
59
  * - file:固定文件,直接看 mtime(全局共享,精度低一档但胜过没有)
60
+ * - artifact:通用形态 —— 解析出一个文件/目录,递归看最新 mtime(2026-08 实机勘探所得规则)
53
61
  * - tail:文件是 jsonl 时才解析尾巴的在途工具调用
54
62
  */
63
+ const h = (...p) => join(homedir(), ...p);
64
+
55
65
  const PROBE_TARGETS = {
56
66
  "claude-code": { dir: claudeSessionDir, ext: ".jsonl", tail: true },
57
67
  codex: { dir: codexSessionDir, ext: ".jsonl", tail: true },
58
68
  "gemini-cli": { dir: geminiSessionDir, ext: "", tail: false },
59
69
  opencode: {
60
- file: () => join(homedir(), ".local", "share", "opencode", "opencode.db-wal"),
70
+ file: () => h(".local", "share", "opencode", "opencode.db-wal"),
61
71
  tail: false,
62
72
  },
73
+ // 按 cwd 定位的:pi(cwd 破折号化子目录)/ grok(URL 编码 cwd)/ kimi(wd_<basename>_<hash>)
74
+ pi: { artifact: () => cwdDashDir(h(".pi", "agent", "sessions")), depth: 2 },
75
+ grok: { artifact: () => existing(join(h(".grok", "sessions"), encodeURIComponent(process.cwd()))), depth: 2 },
76
+ kimi: { artifact: () => wdBasenameDir(h(".kimi-code", "sessions")), depth: 3 },
77
+ // 全局库(会话不分 cwd,精度低一档但真实):goose / hermes / openclaw / mimo
78
+ goose: { artifact: () => existing(h(".local", "share", "goose", "sessions")), depth: 1 },
79
+ hermes: { artifact: () => existing(h(".hermes", "sessions")), depth: 1 },
80
+ openclaw: { artifact: () => existing(h(".openclaw", "agents")), depth: 3 },
81
+ mimo: { artifact: () => existing(h(".local", "share", "mimocode")), depth: 2 },
63
82
  };
64
83
 
84
+ function existing(p) {
85
+ return existsSync(p) ? p : null;
86
+ }
87
+
88
+ /** pi:~/.pi/agent/sessions/ 下找目录名包含 cwd 破折号形式的(取最长匹配) */
89
+ function cwdDashDir(root) {
90
+ try {
91
+ const slug = process.cwd().replace(/\//g, "-");
92
+ const hits = readdirSync(root).filter((d) => d.includes(slug));
93
+ if (hits.length === 0) return null;
94
+ hits.sort((a, b) => a.length - b.length); // 最短的 = 最精确(更长的是子目录会话)
95
+ return join(root, hits[0]);
96
+ } catch {
97
+ return null;
98
+ }
99
+ }
100
+
101
+ /** kimi:~/.kimi-code/sessions/wd_<cwd末级目录名>_<hash>/,同名多个取最近修改 */
102
+ function wdBasenameDir(root) {
103
+ try {
104
+ const base = process.cwd().split("/").filter(Boolean).pop() ?? "";
105
+ const hits = readdirSync(root)
106
+ .filter((d) => d.startsWith(`wd_${base}_`))
107
+ .map((d) => {
108
+ const p = join(root, d);
109
+ return { p, mtime: statSync(p).mtimeMs };
110
+ })
111
+ .sort((a, b) => b.mtime - a.mtime);
112
+ return hits[0]?.p ?? null;
113
+ } catch {
114
+ return null;
115
+ }
116
+ }
117
+
118
+ /** 深度受限的递归最新 mtime(条目上限防大目录) */
119
+ function newestMtimeRec(path, depth, budget = { n: 400 }) {
120
+ try {
121
+ const st = statSync(path);
122
+ if (st.isFile()) return st.mtimeMs;
123
+ if (depth <= 0 || budget.n <= 0) return st.mtimeMs;
124
+ let newest = st.mtimeMs;
125
+ for (const f of readdirSync(path)) {
126
+ if (budget.n-- <= 0) break;
127
+ const m = newestMtimeRec(join(path, f), depth - 1, budget);
128
+ if (m > newest) newest = m;
129
+ }
130
+ return newest;
131
+ } catch {
132
+ return 0;
133
+ }
134
+ }
135
+
65
136
  export function createProbe(harness, processStartMs) {
66
137
  const target = PROBE_TARGETS[harness];
67
138
  const state = {
@@ -76,6 +147,17 @@ export function createProbe(harness, processStartMs) {
76
147
  function bindFile() {
77
148
  if (state.file || !target) return;
78
149
  try {
150
+ // 通用 artifact(文件或目录,递归 mtime)
151
+ if (target.artifact) {
152
+ const p = target.artifact();
153
+ if (!p) return; // 目标还没出现(比如会话目录未创建),下个 tick 再试
154
+ state.file = p;
155
+ state.kind = "file-mtime";
156
+ state.depth = target.depth ?? 1;
157
+ state.lastMtimeMs = newestMtimeRec(p, state.depth);
158
+ state.lastWriteAt = Date.now();
159
+ return;
160
+ }
79
161
  if (target.file) {
80
162
  const p = target.file();
81
163
  if (!existsSync(p)) return;
@@ -111,19 +193,23 @@ export function createProbe(harness, processStartMs) {
111
193
  }
112
194
  }
113
195
 
114
- /** 信号 1:文件最近有写入 */
196
+ /** 信号 1:文件/目录最近有写入 */
115
197
  function recentWrite() {
116
198
  if (!state.file) return false;
117
199
  try {
118
- const st = statSync(state.file);
119
- if (st.mtimeMs > state.lastMtimeMs) {
120
- state.lastMtimeMs = st.mtimeMs;
200
+ const m = state.depth
201
+ ? newestMtimeRec(state.file, state.depth)
202
+ : statSync(state.file).mtimeMs;
203
+ if (m === 0) throw new Error("gone");
204
+ if (m > state.lastMtimeMs) {
205
+ state.lastMtimeMs = m;
121
206
  state.lastWriteAt = Date.now();
122
207
  }
123
208
  return Date.now() - state.lastWriteAt < RECENT_WRITE_MS;
124
209
  } catch {
125
- state.file = null; // 文件消失 → 解绑重找
210
+ state.file = null; // 目标消失 → 解绑重找
126
211
  state.kind = "none";
212
+ state.depth = undefined;
127
213
  return false;
128
214
  }
129
215
  }
package/src/radar.js CHANGED
@@ -18,8 +18,17 @@ const HARNESSES = [
18
18
  ["codex", "codex"],
19
19
  ["opencode", "opencode"],
20
20
  ["gemini", "gemini-cli"],
21
+ ["pi", "pi"],
22
+ ["kimi", "kimi"],
23
+ ["goose", "goose"],
24
+ ["hermes", "hermes"],
25
+ ["openclaw", "openclaw"],
26
+ ["grok", "grok"],
27
+ ["mimo", "mimo"],
21
28
  ];
22
29
 
30
+ const ARTIFACT_DEPTH = { pi: 2, grok: 2, mimo: 2, kimi: 3, openclaw: 3 };
31
+
23
32
  function sh(cmd, args) {
24
33
  try {
25
34
  return execFileSync(cmd, args, { encoding: "utf8", timeout: 5000 });
@@ -69,21 +78,55 @@ function sessionArtifact(harness, cwd) {
69
78
  return join(home, ".local", "share", "opencode", "opencode.db-wal");
70
79
  case "gemini-cli":
71
80
  return cwd ? join(home, ".gemini", "tmp", createHash("sha256").update(cwd).digest("hex")) : null;
81
+ case "pi": {
82
+ if (!cwd) return null;
83
+ try {
84
+ const root = join(home, ".pi", "agent", "sessions");
85
+ const slug = cwd.replace(/\//g, "-");
86
+ const hits = readdirSync(root).filter((d) => d.includes(slug)).sort((a, b) => a.length - b.length);
87
+ return hits[0] ? join(root, hits[0]) : null;
88
+ } catch {
89
+ return null;
90
+ }
91
+ }
92
+ case "grok":
93
+ return cwd ? join(home, ".grok", "sessions", encodeURIComponent(cwd)) : null;
94
+ case "kimi": {
95
+ if (!cwd) return null;
96
+ try {
97
+ const root = join(home, ".kimi-code", "sessions");
98
+ const base = cwd.split("/").filter(Boolean).pop() ?? "";
99
+ const hits = readdirSync(root)
100
+ .filter((d) => d.startsWith(`wd_${base}_`))
101
+ .map((d) => ({ p: join(root, d), m: statSync(join(root, d)).mtimeMs }))
102
+ .sort((a, b) => b.m - a.m);
103
+ return hits[0]?.p ?? null;
104
+ } catch {
105
+ return null;
106
+ }
107
+ }
108
+ case "goose":
109
+ return join(home, ".local", "share", "goose", "sessions");
110
+ case "hermes":
111
+ return join(home, ".hermes", "sessions");
112
+ case "openclaw":
113
+ return join(home, ".openclaw", "agents");
114
+ case "mimo":
115
+ return join(home, ".local", "share", "mimocode");
72
116
  default:
73
117
  return null;
74
118
  }
75
119
  }
76
120
 
77
- function newestMtimeMs(path) {
121
+ function newestMtimeMs(path, depth = 1, budget = { n: 400 }) {
78
122
  try {
79
123
  const st = statSync(path);
80
- if (st.isFile()) return st.mtimeMs;
81
- let newest = 0;
124
+ if (st.isFile() || depth <= 0 || budget.n <= 0) return st.mtimeMs;
125
+ let newest = st.mtimeMs;
82
126
  for (const f of readdirSync(path)) {
83
- try {
84
- const m = statSync(join(path, f)).mtimeMs;
85
- if (m > newest) newest = m;
86
- } catch { /* 忽略单个文件错误 */ }
127
+ if (budget.n-- <= 0) break;
128
+ const m = newestMtimeMs(join(path, f), depth - 1, budget);
129
+ if (m > newest) newest = m;
87
130
  }
88
131
  return newest;
89
132
  } catch {
@@ -187,7 +230,7 @@ export async function runRadar() {
187
230
  if (!active) {
188
231
  const a = sessionArtifact(t.harness, t.cwd);
189
232
  if (a && artifactCount.get(a) === 1 && existsSync(a)) {
190
- if (Date.now() - newestMtimeMs(a) < 90_000) active = true;
233
+ if (Date.now() - newestMtimeMs(a, ARTIFACT_DEPTH[t.harness] ?? 1) < 90_000) active = true;
191
234
  }
192
235
  }
193
236
  if (active) t.activeSeconds += TICK_MS / 1000;