agentosity 0.2.3 → 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 +21 -0
- package/package.json +3 -3
- package/src/index.js +1 -1
- package/src/probes.js +92 -6
- package/src/radar.js +51 -8
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.
|
|
4
|
-
"description": "Agentosity
|
|
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/index.js
CHANGED
|
@@ -177,7 +177,7 @@ async function clockout() {
|
|
|
177
177
|
console.log(`✅ 下班快乐!${r.clocked_local}`);
|
|
178
178
|
if (r.rank_company != null) console.log(` 你是公司今天第 ${r.rank_company} 个下班的 · 全网第 ${r.rank_global} 个`);
|
|
179
179
|
if (r.note) console.log(` ${r.note}`);
|
|
180
|
-
console.log(`
|
|
180
|
+
console.log(` 榜单实时更新:${apiBase()}/me`);
|
|
181
181
|
} else {
|
|
182
182
|
console.error(`打卡失败:${r?.error ?? "网络不可达"}`);
|
|
183
183
|
process.exit(1);
|
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: () =>
|
|
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
|
|
119
|
-
|
|
120
|
-
state.
|
|
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 =
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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;
|