wendkeep 0.71.1 → 0.72.1
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/CHANGELOG.md +64 -3
- package/README.en.md +6 -2
- package/README.md +6 -2
- package/docs/en/commands/observer.md +68 -43
- package/docs/en/commands/sessions-and-import.md +4 -4
- package/docs/pt-BR/commands/observer.md +70 -45
- package/docs/pt-BR/commands/sessions-and-import.md +4 -4
- package/hooks/observer-publish.mjs +1 -0
- package/hooks/understand-inject.mjs +1 -1
- package/package.json +5 -4
- package/packages/integrations/src/host-hooks.mjs +3 -1
- package/packages/vault/src/memory-store.mjs +17 -4
- package/schema/observer/001-authority.sql +107 -0
- package/schema/observer/002-usage.sql +72 -0
- package/schema/observer/003-transcripts.sql +21 -0
- package/src/init.mjs +2 -2
- package/src/observer-auth.mjs +10 -0
- package/src/observer-memory-publish.mjs +15 -9
- package/src/observer-privacy.mjs +23 -0
- package/src/observer-publish.mjs +25 -20
- package/src/observer-server.mjs +274 -40
- package/src/observer-sql-migrate.mjs +335 -0
- package/src/observer-sql-publish.mjs +439 -0
- package/src/observer-sql-store.mjs +573 -0
- package/src/observer-transcript-store.mjs +49 -0
- package/src/observer.mjs +33 -8
- package/src/release-changelog.mjs +1 -1
- package/src/release-provenance.mjs +68 -0
- package/src/taxonomy.mjs +1 -1
- package/src/vault-readme.mjs +2 -2
- package/web/observer/app.mjs +248 -1
- package/web/observer/index.html +1 -0
- package/web/observer/styles.css +34 -1
package/src/observer.mjs
CHANGED
|
@@ -2,18 +2,22 @@ import { homedir } from 'node:os';
|
|
|
2
2
|
import { isAbsolute, resolve } from 'node:path';
|
|
3
3
|
import { appendObserverEvent, readObserverIndex, registerObserverProject } from './observer-store.mjs';
|
|
4
4
|
import { buildProjectSnapshot } from './observer-snapshot.mjs';
|
|
5
|
-
import { compareMemoryParity
|
|
5
|
+
import { compareMemoryParity } from './observer-memory-publish.mjs';
|
|
6
|
+
import { publishObserverSql } from './observer-sql-publish.mjs';
|
|
6
7
|
import { startObserverServer } from './observer-server.mjs';
|
|
8
|
+
import { ensureObserverDatabase, migrateObserverDatabase, listSqlProjects, OBSERVER_SQL_FILE, OBSERVER_SQL_SCHEMA_VERSION } from './observer-sql-store.mjs';
|
|
7
9
|
import { resolveProjectVault } from '../packages/vault/src/project-vault.mjs';
|
|
10
|
+
import { observerAuthHeaders, resolveObserverToken } from './observer-auth.mjs';
|
|
8
11
|
|
|
9
12
|
export const OBSERVER_HELP = `wendkeep observer — Observer local multi-projeto
|
|
10
13
|
|
|
11
14
|
Uso:
|
|
12
15
|
wendkeep observer serve [--data-dir P] [--host 127.0.0.1] [--port 8787]
|
|
13
|
-
[--allow-non-loopback]
|
|
16
|
+
[--allow-non-loopback] [--token TOKEN]
|
|
14
17
|
wendkeep observer register --project P [--vault V] [--data-dir D] [--json]
|
|
15
18
|
wendkeep observer publish --project P [--vault V] [--data-dir D] [--json]
|
|
16
|
-
wendkeep observer memory import --project P [--vault V] [--url U] [--
|
|
19
|
+
wendkeep observer memory import --project P [--vault V] [--url U] [--token TOKEN]
|
|
20
|
+
[--capture-level metadata|messages|full-transcript] [--json]
|
|
17
21
|
wendkeep observer status [--data-dir D] [--json]
|
|
18
22
|
|
|
19
23
|
O Observer local pode manter snapshots operacionais e uma cópia completa da memória em volume
|
|
@@ -54,6 +58,21 @@ function summary(index) {
|
|
|
54
58
|
};
|
|
55
59
|
}
|
|
56
60
|
|
|
61
|
+
function databaseSummary(dir) {
|
|
62
|
+
const db = ensureObserverDatabase(dir);
|
|
63
|
+
try {
|
|
64
|
+
const migrations = migrateObserverDatabase(db);
|
|
65
|
+
return {
|
|
66
|
+
engine: 'sqlite',
|
|
67
|
+
file: OBSERVER_SQL_FILE,
|
|
68
|
+
schema_version: OBSERVER_SQL_SCHEMA_VERSION,
|
|
69
|
+
migrations: migrations.applied.length,
|
|
70
|
+
projects: listSqlProjects(db).length,
|
|
71
|
+
ready: true,
|
|
72
|
+
};
|
|
73
|
+
} finally { db.close(); }
|
|
74
|
+
}
|
|
75
|
+
|
|
57
76
|
export async function runObserver(argv = [], { write = (chunk) => process.stdout.write(chunk) } = {}) {
|
|
58
77
|
const [sub] = argv;
|
|
59
78
|
const asJson = argv.includes('--json');
|
|
@@ -62,9 +81,10 @@ export async function runObserver(argv = [], { write = (chunk) => process.stdout
|
|
|
62
81
|
return 0;
|
|
63
82
|
}
|
|
64
83
|
const dir = dataDir(argv);
|
|
84
|
+
const token = resolveObserverToken(optionValue(argv, '--token'));
|
|
65
85
|
|
|
66
86
|
if (sub === 'status') {
|
|
67
|
-
print(summary(readObserverIndex(dir)), asJson, write);
|
|
87
|
+
print({ ...summary(readObserverIndex(dir)), database: databaseSummary(dir) }, asJson, write);
|
|
68
88
|
return 0;
|
|
69
89
|
}
|
|
70
90
|
|
|
@@ -75,6 +95,7 @@ export async function runObserver(argv = [], { write = (chunk) => process.stdout
|
|
|
75
95
|
host,
|
|
76
96
|
port: Number(optionValue(argv, '--port') || 8787),
|
|
77
97
|
allowNonLoopback: argv.includes('--allow-non-loopback'),
|
|
98
|
+
token,
|
|
78
99
|
});
|
|
79
100
|
const address = server.address();
|
|
80
101
|
process.stdout.write(`wendkeep observer listening: http://${address.address}:${address.port}\n`);
|
|
@@ -89,7 +110,7 @@ export async function runObserver(argv = [], { write = (chunk) => process.stdout
|
|
|
89
110
|
const snapshot = buildProjectSnapshot({ vaultBase: vault, projectRoot: root });
|
|
90
111
|
const url = optionValue(argv, '--url') || process.env.WENDKEEP_OBSERVER_URL || '';
|
|
91
112
|
if (!url) throw new Error('observer memory import: --url ou WENDKEEP_OBSERVER_URL é obrigatório.');
|
|
92
|
-
const headers = { 'content-type': 'application/json', accept: 'application/json' };
|
|
113
|
+
const headers = observerAuthHeaders(token, { 'content-type': 'application/json', accept: 'application/json' });
|
|
93
114
|
const registration = await fetch(
|
|
94
115
|
String(url).replace(/\/$/, '') + '/v1/projects/' + encodeURIComponent(snapshot.project_id),
|
|
95
116
|
{
|
|
@@ -103,20 +124,24 @@ export async function runObserver(argv = [], { write = (chunk) => process.stdout
|
|
|
103
124
|
},
|
|
104
125
|
);
|
|
105
126
|
if (!registration.ok) throw new Error('Observer não registrou o projeto: HTTP ' + registration.status + '.');
|
|
106
|
-
const
|
|
127
|
+
const sql = await publishObserverSql({
|
|
107
128
|
vaultBase: vault,
|
|
108
129
|
projectId: snapshot.project_id,
|
|
109
130
|
url,
|
|
131
|
+
token,
|
|
132
|
+
captureLevel: optionValue(argv, '--capture-level') || process.env.WENDKEEP_OBSERVER_CAPTURE_LEVEL || 'metadata',
|
|
110
133
|
});
|
|
111
134
|
const parity = await compareMemoryParity({
|
|
112
135
|
vaultBase: vault,
|
|
113
136
|
projectId: snapshot.project_id,
|
|
114
137
|
url,
|
|
138
|
+
token,
|
|
115
139
|
});
|
|
116
140
|
const result = {
|
|
117
|
-
ok:
|
|
141
|
+
ok: sql.ok && parity.missing === 0 && parity.mismatched === 0,
|
|
118
142
|
project_id: snapshot.project_id,
|
|
119
|
-
|
|
143
|
+
sql,
|
|
144
|
+
memory: { ...sql, authority: 'sqlite' },
|
|
120
145
|
parity,
|
|
121
146
|
};
|
|
122
147
|
print(result, asJson, write);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Pure helper: extract a single version's release notes from a Keep-a-Changelog
|
|
2
|
-
// file. Reused by scripts/release.mjs and .github/workflows/
|
|
2
|
+
// file. Reused by scripts/release.mjs and .github/workflows/auto-tag.yml so the
|
|
3
3
|
// GitHub Release body always matches the committed CHANGELOG.
|
|
4
4
|
|
|
5
5
|
const HEADER_RE = /^##\s*\[([^\]]+)\]\s*[—–-]\s*(.+?)\s*$/;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const DEPENDENCY_FIELDS = Object.freeze([
|
|
2
|
+
'dependencies',
|
|
3
|
+
'devDependencies',
|
|
4
|
+
'optionalDependencies',
|
|
5
|
+
'peerDependencies',
|
|
6
|
+
]);
|
|
7
|
+
|
|
8
|
+
export function packageHasSelfDependency(pkg = {}) {
|
|
9
|
+
const name = String(pkg.name || '');
|
|
10
|
+
if (!name) return false;
|
|
11
|
+
return DEPENDENCY_FIELDS.some((field) => Object.hasOwn(pkg[field] || {}, name));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function evaluateReleaseProvenance({
|
|
15
|
+
name,
|
|
16
|
+
version,
|
|
17
|
+
headCommit,
|
|
18
|
+
tagCommit = '',
|
|
19
|
+
publishedIntegrity = '',
|
|
20
|
+
localIntegrity = '',
|
|
21
|
+
requirePublished = false,
|
|
22
|
+
} = {}) {
|
|
23
|
+
const tag = `v${version}`;
|
|
24
|
+
if (tagCommit && tagCommit !== headCommit) {
|
|
25
|
+
return {
|
|
26
|
+
ok: false,
|
|
27
|
+
code: 'tag_commit_mismatch',
|
|
28
|
+
message: `${tag} aponta para ${tagCommit.slice(0, 7)}, não para ${headCommit.slice(0, 7)}. Bump a versão antes de alterar a árvore publicada.`,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (publishedIntegrity && !tagCommit) {
|
|
32
|
+
return {
|
|
33
|
+
ok: false,
|
|
34
|
+
code: 'published_tag_missing',
|
|
35
|
+
message: `${name}@${version} está publicado, mas ${tag} não comprova o commit correspondente.`,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (requirePublished && !publishedIntegrity) {
|
|
39
|
+
return {
|
|
40
|
+
ok: false,
|
|
41
|
+
code: 'published_artifact_missing',
|
|
42
|
+
message: `${name}@${version} ainda não possui integridade consultável no npm.`,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (publishedIntegrity && localIntegrity && publishedIntegrity !== localIntegrity) {
|
|
46
|
+
return {
|
|
47
|
+
ok: false,
|
|
48
|
+
code: 'tarball_integrity_mismatch',
|
|
49
|
+
message: `o tarball de ${tag} diverge do artefato publicado no npm.`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (publishedIntegrity && !localIntegrity) {
|
|
53
|
+
return {
|
|
54
|
+
ok: false,
|
|
55
|
+
code: 'local_integrity_missing',
|
|
56
|
+
message: `não foi possível calcular a integridade do tarball de ${tag}.`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
ok: true,
|
|
61
|
+
code: publishedIntegrity ? 'verified' : 'release_candidate',
|
|
62
|
+
name,
|
|
63
|
+
version,
|
|
64
|
+
tag,
|
|
65
|
+
commit: headCommit,
|
|
66
|
+
integrity: publishedIntegrity || localIntegrity || '',
|
|
67
|
+
};
|
|
68
|
+
}
|
package/src/taxonomy.mjs
CHANGED
|
@@ -131,7 +131,7 @@ export const RUNNABLE_HOOKS = [
|
|
|
131
131
|
// the MOST agent-agnostic mechanism it supports; the Claude Code plugin entry
|
|
132
132
|
// (extraKnownMarketplaces + enabledPlugins) is an additive bonus, never the base.
|
|
133
133
|
// - mcp: .mcp.json server entry (works on any MCP-capable agent)
|
|
134
|
-
// - wendkeepHook: a wendkeep-authored hook (runs via `npx wendkeep hook`, any agent)
|
|
134
|
+
// - wendkeepHook: a wendkeep-authored hook (runs via `npx --no-install wendkeep hook`, any agent)
|
|
135
135
|
// - installer: cross-agent install script (used on non-Claude agents)
|
|
136
136
|
// marketplace/plugin shapes are verified against a real ~/.claude/settings.json.
|
|
137
137
|
export const COMPANIONS = [
|
package/src/vault-readme.mjs
CHANGED
|
@@ -52,7 +52,7 @@ export function renderVaultReadme({ projectName, vaultPath, withMcp = true, loca
|
|
|
52
52
|
const mcpIntro = withMcp ? ', and read/written by the **MCPVault** MCP server' : '';
|
|
53
53
|
const access = [`- **Obsidian:** open this folder with "Open folder as vault" → \`${vaultPath}\``];
|
|
54
54
|
if (withMcp) access.push('- **Agent (MCP):** the `wendkeep-vault` server (MCPVault) points at this vault (set in `.mcp.json`), giving the agent read/write on the notes.');
|
|
55
|
-
access.push('- **Hooks:** Codex and Claude Code call `npx wendkeep hook <name>`; the vault is discovered from the project-local `.wendkeep.json` binding and checked against `.brain/PROJECT.json`.');
|
|
55
|
+
access.push('- **Hooks:** Codex and Claude Code call `npx --no-install wendkeep hook <name>`; the vault is discovered from the project-local `.wendkeep.json` binding and checked against `.brain/PROJECT.json`.');
|
|
56
56
|
return `# Obsidian vault — ${name}
|
|
57
57
|
|
|
58
58
|
> Knowledge base of **${name}**, captured automatically by wendkeep from AI coding-agent
|
|
@@ -84,7 +84,7 @@ ${access.join('\n')}
|
|
|
84
84
|
const mcpIntro = withMcp ? ', e lida/escrita pelo MCP server **MCPVault**' : '';
|
|
85
85
|
const access = [`- **Obsidian:** abra esta pasta com "Open folder as vault" → \`${vaultPath}\``];
|
|
86
86
|
if (withMcp) access.push('- **Agente (MCP):** o servidor `wendkeep-vault` (MCPVault) é apontado para este vault pelo `wendkeep init` (em `.mcp.json`), dando ao agente leitura/escrita das notas.');
|
|
87
|
-
access.push('- **Hooks:** Codex e Claude Code chamam `npx wendkeep hook <name>`; o vault é descoberto pelo vínculo local `.wendkeep.json` e validado contra `.brain/PROJECT.json`.');
|
|
87
|
+
access.push('- **Hooks:** Codex e Claude Code chamam `npx --no-install wendkeep hook <name>`; o vault é descoberto pelo vínculo local `.wendkeep.json` e validado contra `.brain/PROJECT.json`.');
|
|
88
88
|
return `# Vault Obsidian — ${name}
|
|
89
89
|
|
|
90
90
|
> Base de conhecimento de **${name}**, capturada automaticamente pelo wendkeep a
|
package/web/observer/app.mjs
CHANGED
|
@@ -96,11 +96,82 @@ export async function loadProjectMemory(fetchImpl = globalThis.fetch, projectId
|
|
|
96
96
|
return { tree, sync };
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
export function usageQuery(filters = {}) {
|
|
100
|
+
const params = new URLSearchParams();
|
|
101
|
+
const keys = ['from', 'to', 'agent_id', 'subagent_id', 'provider', 'model_provider', 'model', 'change', 'session_id', 'role'];
|
|
102
|
+
for (const key of keys) {
|
|
103
|
+
const value = filters[key] ?? filters[key.replace('_id', 'Id')] ?? '';
|
|
104
|
+
if (String(value).trim()) params.set(key, String(value));
|
|
105
|
+
}
|
|
106
|
+
const query = params.toString();
|
|
107
|
+
return query ? `?${query}` : '';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function loadProjectUsage(fetchImpl = globalThis.fetch, projectId = '', filters = {}) {
|
|
111
|
+
const id = encodeURIComponent(projectId);
|
|
112
|
+
const query = usageQuery(filters);
|
|
113
|
+
const [summary, breakdown, calls] = await Promise.all([
|
|
114
|
+
requestJson(fetchImpl, `/v1/projects/${id}/usage/summary${query}`),
|
|
115
|
+
requestJson(fetchImpl, `/v1/projects/${id}/usage/breakdown${query}`),
|
|
116
|
+
requestJson(fetchImpl, `/v1/projects/${id}/usage/calls${query}`),
|
|
117
|
+
]);
|
|
118
|
+
return { summary, breakdown, calls, filters: { ...filters } };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function buildUsageViewModel(usage = {}) {
|
|
122
|
+
const summary = usage.summary || {};
|
|
123
|
+
const agents = (Array.isArray(usage.breakdown?.agents) ? usage.breakdown.agents : []).map((agent) => ({ ...agent, children: [] }));
|
|
124
|
+
const byId = new Map(agents.map((agent) => [agent.agent_id, agent]));
|
|
125
|
+
for (const agent of agents) {
|
|
126
|
+
const parent = agent.parent_agent_id ? byId.get(agent.parent_agent_id) : null;
|
|
127
|
+
if (parent && parent !== agent) parent.children.push(agent);
|
|
128
|
+
}
|
|
129
|
+
const roots = agents.filter((agent) => !agent.parent_agent_id || !byId.has(agent.parent_agent_id));
|
|
130
|
+
const tokens = {
|
|
131
|
+
input: Number(summary.tokens?.input || 0),
|
|
132
|
+
cacheWrite: Number(summary.tokens?.cache_write || 0),
|
|
133
|
+
cacheRead: Number(summary.tokens?.cache_read || 0),
|
|
134
|
+
output: Number(summary.tokens?.output || 0),
|
|
135
|
+
reasoning: Number(summary.tokens?.reasoning || 0),
|
|
136
|
+
total: Number(summary.tokens?.total || 0),
|
|
137
|
+
};
|
|
138
|
+
const coverage = summary.coverage || {};
|
|
139
|
+
const complete = Number(coverage.complete || 0);
|
|
140
|
+
const summaryOnly = Number(coverage.summary_only || 0);
|
|
141
|
+
return {
|
|
142
|
+
totalCost: Number(summary.cost_usd || 0),
|
|
143
|
+
mainCost: Number(summary.main_cost_usd || 0),
|
|
144
|
+
subagentCost: Number(summary.subagent_cost_usd || 0),
|
|
145
|
+
wastedCost: Number(summary.wasted_usd || 0),
|
|
146
|
+
calls: Array.isArray(usage.calls?.calls) ? usage.calls.calls : [],
|
|
147
|
+
callCount: Number(summary.calls || usage.calls?.total || 0),
|
|
148
|
+
sessions: Number(summary.sessions || 0),
|
|
149
|
+
agentsCount: Number(summary.agents || 0),
|
|
150
|
+
subagentsCount: Number(summary.subagents || 0),
|
|
151
|
+
modelsCount: Number(summary.models || 0),
|
|
152
|
+
tokens,
|
|
153
|
+
trend: Array.isArray(summary.by_day) ? summary.by_day : [],
|
|
154
|
+
agents: roots,
|
|
155
|
+
allAgents: agents,
|
|
156
|
+
coverage: {
|
|
157
|
+
complete,
|
|
158
|
+
summaryOnly,
|
|
159
|
+
total: Number(coverage.transcripts || complete + summaryOnly),
|
|
160
|
+
label: `${complete} completo${complete === 1 ? '' : 's'} · ${summaryOnly} agregado${summaryOnly === 1 ? '' : 's'}`,
|
|
161
|
+
},
|
|
162
|
+
hasUnknownPricing: Number(summary.unknown_priced_rollups || 0) > 0,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
99
166
|
export async function loadMemoryDocument(fetchImpl = globalThis.fetch, projectId = '', logicalPath = '') {
|
|
100
167
|
const query = new URLSearchParams({ path: logicalPath });
|
|
101
168
|
return requestJson(fetchImpl, '/v1/projects/' + encodeURIComponent(projectId) + '/memory/document?' + query.toString());
|
|
102
169
|
}
|
|
103
170
|
|
|
171
|
+
export async function loadProjectTranscript(fetchImpl = globalThis.fetch, projectId = '', transcriptId = '') {
|
|
172
|
+
return requestJson(fetchImpl, `/v1/projects/${encodeURIComponent(projectId)}/transcripts/${encodeURIComponent(transcriptId)}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
104
175
|
export async function searchProjectMemory(fetchImpl = globalThis.fetch, projectId = '', query = '') {
|
|
105
176
|
const params = new URLSearchParams({ q: query });
|
|
106
177
|
return requestJson(fetchImpl, '/v1/projects/' + encodeURIComponent(projectId) + '/memory/search?' + params.toString());
|
|
@@ -401,6 +472,165 @@ function renderSync(container, sync, projectId = '') {
|
|
|
401
472
|
container.replaceChildren(heading, facts, note, exportLink);
|
|
402
473
|
}
|
|
403
474
|
|
|
475
|
+
function formatNumber(value) {
|
|
476
|
+
return new Intl.NumberFormat('pt-BR').format(Number(value) || 0);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function formatUsd(value) {
|
|
480
|
+
return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'USD', minimumFractionDigits: 4 }).format(Number(value) || 0);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function usageFilter(label, name, value, type = 'text') {
|
|
484
|
+
const wrapper = node('label', 'usage-filter');
|
|
485
|
+
wrapper.append(node('span', '', label));
|
|
486
|
+
const input = node('input');
|
|
487
|
+
input.name = name;
|
|
488
|
+
input.type = type;
|
|
489
|
+
input.value = String(value || '');
|
|
490
|
+
wrapper.append(input);
|
|
491
|
+
return wrapper;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function usageSelect(label, name, value, options) {
|
|
495
|
+
const wrapper = node('label', 'usage-filter');
|
|
496
|
+
wrapper.append(node('span', '', label));
|
|
497
|
+
const select = node('select');
|
|
498
|
+
select.name = name;
|
|
499
|
+
for (const [optionValue, optionLabel] of options) {
|
|
500
|
+
const option = node('option', '', optionLabel);
|
|
501
|
+
option.value = optionValue;
|
|
502
|
+
option.selected = optionValue === String(value || '');
|
|
503
|
+
select.append(option);
|
|
504
|
+
}
|
|
505
|
+
wrapper.append(select);
|
|
506
|
+
return wrapper;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function renderUsage(container, projectId, usage, { onFiltersChanged, onTranscript } = {}) {
|
|
510
|
+
const view = buildUsageViewModel(usage);
|
|
511
|
+
const filters = usage?.filters || {};
|
|
512
|
+
const heading = node('div', 'workspace-section-heading');
|
|
513
|
+
heading.append(node('p', 'eyebrow', 'PROJECT CONSUMPTION'), node('h2', '', 'Consumo'));
|
|
514
|
+
|
|
515
|
+
const filterForm = node('form', 'usage-filters');
|
|
516
|
+
filterForm.append(
|
|
517
|
+
usageFilter('De', 'from', filters.from, 'date'),
|
|
518
|
+
usageFilter('Até', 'to', filters.to, 'date'),
|
|
519
|
+
usageSelect('Escopo', 'role', filters.role, [['', 'Principal + subagentes'], ['main', 'Principal'], ['subagent', 'Subagentes']]),
|
|
520
|
+
usageFilter('Agente', 'agent_id', filters.agent_id || filters.agentId),
|
|
521
|
+
usageFilter('Subagente', 'subagent_id', filters.subagent_id || filters.subagentId),
|
|
522
|
+
usageFilter('Provedor', 'provider', filters.provider),
|
|
523
|
+
usageFilter('Provedor do modelo', 'model_provider', filters.model_provider || filters.modelProvider),
|
|
524
|
+
usageFilter('Modelo', 'model', filters.model),
|
|
525
|
+
usageFilter('Change', 'change', filters.change || filters.changeSlug),
|
|
526
|
+
usageFilter('Sessão', 'session_id', filters.session_id || filters.sessionId),
|
|
527
|
+
node('button', 'button-primary', 'Aplicar filtros'),
|
|
528
|
+
);
|
|
529
|
+
filterForm.addEventListener('submit', (event) => {
|
|
530
|
+
event.preventDefault();
|
|
531
|
+
const form = new FormData(filterForm);
|
|
532
|
+
onFiltersChanged?.({
|
|
533
|
+
from: form.get('from') || '', to: form.get('to') || '', role: form.get('role') || '',
|
|
534
|
+
agent_id: form.get('agent_id') || '', subagent_id: form.get('subagent_id') || '',
|
|
535
|
+
provider: form.get('provider') || '', model_provider: form.get('model_provider') || '', model: form.get('model') || '',
|
|
536
|
+
change: form.get('change') || '', session_id: form.get('session_id') || '',
|
|
537
|
+
});
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
const cards = node('div', 'usage-summary-grid');
|
|
541
|
+
const summaryCards = [
|
|
542
|
+
['Custo total', formatUsd(view.totalCost), `${formatUsd(view.mainCost)} principal · ${formatUsd(view.subagentCost)} subagentes`],
|
|
543
|
+
['Tokens', formatNumber(view.tokens.total), `entrada ${formatNumber(view.tokens.input)} · saída ${formatNumber(view.tokens.output)}`],
|
|
544
|
+
['Chamadas', formatNumber(view.callCount), `${formatNumber(view.sessions)} sessões · ${formatNumber(view.modelsCount)} modelos`],
|
|
545
|
+
['Cobertura', view.coverage.label, `${formatNumber(view.coverage.total)} transcript(s)`],
|
|
546
|
+
];
|
|
547
|
+
for (const [label, value, caption] of summaryCards) {
|
|
548
|
+
const card = node('article', 'usage-summary-card');
|
|
549
|
+
card.append(node('span', 'metric-label', label), node('strong', 'usage-summary-value', value), node('span', 'metric-caption', caption));
|
|
550
|
+
cards.append(card);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const content = [];
|
|
554
|
+
if (view.hasUnknownPricing) content.push(node('div', 'usage-warning', 'Há modelos sem tarifa conhecida. Os custos históricos registrados não foram recalculados.'));
|
|
555
|
+
if (view.wastedCost > 0) content.push(node('div', 'usage-callout', `Desperdício registrado em workflows interrompidos: ${formatUsd(view.wastedCost)}.`));
|
|
556
|
+
|
|
557
|
+
const tokenPanel = node('section', 'usage-panel');
|
|
558
|
+
tokenPanel.append(node('h3', '', 'Tokens por categoria'));
|
|
559
|
+
const tokenGrid = node('div', 'usage-token-grid');
|
|
560
|
+
for (const [label, value] of [['Entrada', view.tokens.input], ['Cache write', view.tokens.cacheWrite], ['Cache read', view.tokens.cacheRead], ['Saída', view.tokens.output], ['Reasoning', view.tokens.reasoning], ['Total', view.tokens.total]]) {
|
|
561
|
+
tokenGrid.append(fact(label, formatNumber(value)));
|
|
562
|
+
}
|
|
563
|
+
tokenPanel.append(tokenGrid);
|
|
564
|
+
content.push(tokenPanel);
|
|
565
|
+
|
|
566
|
+
const trendPanel = node('section', 'usage-panel');
|
|
567
|
+
trendPanel.append(node('h3', '', 'Tendência diária'));
|
|
568
|
+
const trend = node('div', 'usage-trend');
|
|
569
|
+
if (!view.trend.length) trend.append(node('p', 'muted', 'Sem atividade no período selecionado.'));
|
|
570
|
+
else for (const day of view.trend) {
|
|
571
|
+
const row = node('div', 'usage-trend-row');
|
|
572
|
+
row.append(node('span', '', day.date), node('span', '', `${formatNumber(day.tokens_total)} tokens`), node('strong', '', formatUsd(day.cost_usd)));
|
|
573
|
+
trend.append(row);
|
|
574
|
+
}
|
|
575
|
+
trendPanel.append(trend);
|
|
576
|
+
content.push(trendPanel);
|
|
577
|
+
|
|
578
|
+
const hierarchy = node('section', 'usage-panel usage-hierarchy');
|
|
579
|
+
hierarchy.append(node('h3', '', 'Agentes, modelos e subagentes'));
|
|
580
|
+
if (!view.agents.length) hierarchy.append(node('p', 'muted', 'Nenhum agente com consumo no período.'));
|
|
581
|
+
const renderAgent = (agent) => {
|
|
582
|
+
const details = node('details', 'usage-agent');
|
|
583
|
+
const summary = node('summary');
|
|
584
|
+
summary.append(node('strong', '', agent.agent_name || agent.agent_id), node('span', 'usage-agent-meta', `${agent.role} · ${formatNumber(agent.tokens_total)} tokens · ${formatUsd(agent.cost_usd)}`));
|
|
585
|
+
details.append(summary);
|
|
586
|
+
const models = node('div', 'usage-model-list');
|
|
587
|
+
for (const model of agent.models || []) {
|
|
588
|
+
const row = node('div', 'usage-model-row');
|
|
589
|
+
row.append(node('span', '', `${model.model_provider || '—'} · ${model.model || 'modelo sem nome'}`), node('span', '', `${formatNumber(model.tokens_total)} tokens`), node('strong', '', formatUsd(model.cost_usd)));
|
|
590
|
+
models.append(row);
|
|
591
|
+
}
|
|
592
|
+
details.append(models);
|
|
593
|
+
for (const child of agent.children || []) details.append(renderAgent(child));
|
|
594
|
+
return details;
|
|
595
|
+
};
|
|
596
|
+
for (const agent of view.agents) hierarchy.append(renderAgent(agent));
|
|
597
|
+
content.push(hierarchy);
|
|
598
|
+
|
|
599
|
+
const callsPanel = node('section', 'usage-panel usage-calls');
|
|
600
|
+
callsPanel.append(node('h3', '', 'Chamadas e transcripts'));
|
|
601
|
+
if (!view.calls.length) callsPanel.append(node('p', 'muted', 'Nenhuma chamada individual disponível; históricos agregados aparecem acima.'));
|
|
602
|
+
for (const call of view.calls) {
|
|
603
|
+
const details = node('details', 'usage-call');
|
|
604
|
+
const summary = node('summary');
|
|
605
|
+
summary.append(node('strong', '', call.model || 'modelo sem nome'), node('span', 'usage-agent-meta', `${call.role || 'principal'} · ${formatNumber(call.tokens?.total)} tokens · ${formatUsd(call.cost_usd)}`));
|
|
606
|
+
details.append(summary);
|
|
607
|
+
const copy = node('div', 'usage-call-copy');
|
|
608
|
+
copy.append(node('p', '', `Sessão: ${call.session_id || '—'} · Agente: ${call.agent_id || '—'}`));
|
|
609
|
+
copy.append(node('h4', '', 'Prompt'), node('pre', '', call.prompt || '—'), node('h4', '', 'Resposta'), node('pre', '', call.response || '—'));
|
|
610
|
+
if (call.transcript_id) {
|
|
611
|
+
const transcriptButton = node('button', 'reader-toggle', 'Abrir transcript completo');
|
|
612
|
+
const transcriptTarget = node('pre', 'usage-transcript', 'Carregando transcript…');
|
|
613
|
+
transcriptTarget.hidden = true;
|
|
614
|
+
transcriptButton.addEventListener('click', async () => {
|
|
615
|
+
transcriptTarget.hidden = false;
|
|
616
|
+
if (transcriptTarget.dataset.loaded) return;
|
|
617
|
+
try {
|
|
618
|
+
const transcript = await onTranscript?.(call.transcript_id);
|
|
619
|
+
transcriptTarget.textContent = transcript?.content || 'Transcript vazio.';
|
|
620
|
+
transcriptTarget.dataset.loaded = 'true';
|
|
621
|
+
} catch (error) {
|
|
622
|
+
transcriptTarget.textContent = error.message || 'Transcript indisponível.';
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
copy.append(transcriptButton, transcriptTarget);
|
|
626
|
+
}
|
|
627
|
+
details.append(copy);
|
|
628
|
+
callsPanel.append(details);
|
|
629
|
+
}
|
|
630
|
+
content.push(callsPanel);
|
|
631
|
+
container.replaceChildren(heading, filterForm, cards, ...content);
|
|
632
|
+
}
|
|
633
|
+
|
|
404
634
|
function renderReader(container, document) {
|
|
405
635
|
const heading = node('div', 'reader-heading');
|
|
406
636
|
heading.append(node('p', 'eyebrow', document.category), node('h2', '', document.title));
|
|
@@ -438,6 +668,8 @@ function startDashboardV2() {
|
|
|
438
668
|
selectedId: '',
|
|
439
669
|
filter: '',
|
|
440
670
|
memory: new Map(),
|
|
671
|
+
usage: new Map(),
|
|
672
|
+
usageFilters: new Map(),
|
|
441
673
|
route: parseObserverRoute(globalThis.location?.hash || ''),
|
|
442
674
|
};
|
|
443
675
|
const dashboardError = byId('dashboard-error');
|
|
@@ -497,6 +729,12 @@ function startDashboardV2() {
|
|
|
497
729
|
if (!state.memory.has(projectId)) state.memory.set(projectId, await loadProjectMemory(fetchJson, projectId));
|
|
498
730
|
return state.memory.get(projectId);
|
|
499
731
|
};
|
|
732
|
+
const ensureProjectUsage = async (projectId) => {
|
|
733
|
+
const filters = state.usageFilters.get(projectId) || {};
|
|
734
|
+
const usage = await loadProjectUsage(fetchJson, projectId, filters);
|
|
735
|
+
state.usage.set(projectId, usage);
|
|
736
|
+
return usage;
|
|
737
|
+
};
|
|
500
738
|
const renderSearchRoute = async (query) => {
|
|
501
739
|
const model = state.models.find((item) => item.projectId === state.selectedId) || state.models[0];
|
|
502
740
|
setHidden(dashboardPanel, true);
|
|
@@ -542,7 +780,16 @@ function startDashboardV2() {
|
|
|
542
780
|
renderReader(workspaceContent, buildMemoryDocumentViewModel(payload, payload.content));
|
|
543
781
|
return;
|
|
544
782
|
}
|
|
545
|
-
if (route.section === '
|
|
783
|
+
if (route.section === 'usage') {
|
|
784
|
+
const usage = await ensureProjectUsage(model.projectId);
|
|
785
|
+
renderUsage(workspaceContent, model.projectId, usage, {
|
|
786
|
+
onFiltersChanged: (filters) => {
|
|
787
|
+
state.usageFilters.set(model.projectId, filters);
|
|
788
|
+
renderWorkspaceRoute({ ...route });
|
|
789
|
+
},
|
|
790
|
+
onTranscript: (transcriptId) => loadProjectTranscript(fetchJson, model.projectId, transcriptId),
|
|
791
|
+
});
|
|
792
|
+
} else if (route.section === 'sessions') renderSessions(workspaceContent, model.projectId, documents);
|
|
546
793
|
else if (route.section === 'memory') renderMemory(workspaceContent, model.projectId, documents);
|
|
547
794
|
else if (route.section === 'changes') renderChanges(workspaceContent, model.projectId, documents);
|
|
548
795
|
else if (route.section === 'sync') renderSync(workspaceContent, memory.sync, model.projectId);
|
package/web/observer/index.html
CHANGED
|
@@ -96,6 +96,7 @@
|
|
|
96
96
|
<aside class="workspace-rail panel" aria-label="Navegação do projeto">
|
|
97
97
|
<nav id="workspace-nav" class="workspace-nav">
|
|
98
98
|
<a data-workspace-section="overview" href="#project/project-a/overview">Overview</a>
|
|
99
|
+
<a data-workspace-section="usage" href="#project/project-a/usage">Consumo</a>
|
|
99
100
|
<a data-workspace-section="sessions" href="#project/project-a/sessions">Sessões</a>
|
|
100
101
|
<a data-workspace-section="memory" href="#project/project-a/memory">Memória</a>
|
|
101
102
|
<a data-workspace-section="changes" href="#project/project-a/changes">Changes</a>
|
package/web/observer/styles.css
CHANGED
|
@@ -183,6 +183,33 @@ input::placeholder { color: var(--faint); }
|
|
|
183
183
|
.markdown-gap { height: .35em; }
|
|
184
184
|
.sync-callout, .workspace-error { margin-top: 24px; padding: 17px; border: 1px solid rgba(156, 224, 198, .2); border-radius: var(--radius-sm); color: var(--muted); background: rgba(156, 224, 198, .05); font-size: .8rem; line-height: 1.5; }
|
|
185
185
|
.workspace-error { border-color: rgba(245, 139, 131, .28); color: var(--red); background: rgba(245, 139, 131, .07); }
|
|
186
|
+
.usage-filters { display: flex; flex-wrap: wrap; align-items: end; gap: 9px; margin: 20px 0; padding: 13px; border: 1px solid var(--line); border-radius: var(--radius-sm); background: rgba(7, 16, 21, .28); }
|
|
187
|
+
.usage-filter { display: grid; gap: 5px; min-width: 112px; color: var(--faint); font-size: .61rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; }
|
|
188
|
+
.usage-filter input, .usage-filter select { min-width: 0; padding: 8px 9px; border: 1px solid var(--line); border-radius: 6px; color: var(--text); background: rgba(7, 16, 21, .65); font-size: .73rem; text-transform: none; }
|
|
189
|
+
.button-primary { padding: 9px 12px; border: 1px solid rgba(156, 224, 198, .3); border-radius: 7px; color: var(--ink-950); background: var(--mint); font-size: .7rem; font-weight: 700; }
|
|
190
|
+
.button-primary:hover { background: var(--mint-bright); }
|
|
191
|
+
.usage-summary-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 9px; margin-bottom: 14px; }
|
|
192
|
+
.usage-summary-card { min-width: 0; padding: 15px; border: 1px solid var(--line); border-radius: var(--radius-md); background: rgba(20, 43, 50, .45); }
|
|
193
|
+
.usage-summary-value { display: block; overflow: hidden; margin: 10px 0 5px; color: var(--mint-bright); font-family: Georgia, serif; font-size: clamp(1.15rem, 2vw, 1.75rem); font-weight: 400; text-overflow: ellipsis; white-space: nowrap; }
|
|
194
|
+
.usage-warning { margin-bottom: 12px; padding: 13px 15px; border: 1px solid rgba(246, 189, 114, .3); border-radius: var(--radius-sm); color: var(--amber); background: rgba(246, 189, 114, .07); font-size: .76rem; line-height: 1.45; }
|
|
195
|
+
.usage-panel { margin-top: 14px; padding: 17px; border: 1px solid var(--line); border-radius: var(--radius-md); background: rgba(7, 16, 21, .25); }
|
|
196
|
+
.usage-panel h3 { margin: 0 0 13px; color: var(--mint-bright); font-size: .82rem; letter-spacing: .06em; text-transform: uppercase; }
|
|
197
|
+
.usage-token-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 7px; }
|
|
198
|
+
.usage-token-grid .fact { min-width: 0; padding: 10px; }
|
|
199
|
+
.usage-trend { display: grid; gap: 6px; }
|
|
200
|
+
.usage-trend-row, .usage-model-row { display: grid; grid-template-columns: minmax(100px, 1fr) auto auto; gap: 12px; align-items: center; padding: 9px 11px; border-radius: 7px; color: var(--muted); background: rgba(20, 43, 50, .38); font-size: .72rem; }
|
|
201
|
+
.usage-trend-row strong, .usage-model-row strong { color: var(--mint); font-weight: 600; }
|
|
202
|
+
.usage-agent { border-top: 1px solid var(--line); }
|
|
203
|
+
.usage-agent:first-of-type { border-top: 0; }
|
|
204
|
+
.usage-agent summary { display: flex; justify-content: space-between; gap: 12px; padding: 12px 2px; cursor: pointer; color: var(--text); font-size: .78rem; }
|
|
205
|
+
.usage-agent-meta { color: var(--muted); font-size: .68rem; font-weight: 400; }
|
|
206
|
+
.usage-model-list { display: grid; gap: 6px; padding: 0 0 12px 17px; }
|
|
207
|
+
.usage-call { border-top: 1px solid var(--line); }
|
|
208
|
+
.usage-call:first-of-type { border-top: 0; }
|
|
209
|
+
.usage-call summary { display: flex; justify-content: space-between; gap: 12px; padding: 11px 2px; cursor: pointer; font-size: .76rem; }
|
|
210
|
+
.usage-call-copy { padding: 0 0 15px 17px; color: var(--muted); font-size: .72rem; }
|
|
211
|
+
.usage-call-copy h4 { margin: 13px 0 6px; color: var(--faint); font-size: .63rem; letter-spacing: .1em; text-transform: uppercase; }
|
|
212
|
+
.usage-call-copy pre, .usage-transcript { max-height: 260px; overflow: auto; margin: 6px 0; padding: 11px; border: 1px solid var(--line); border-radius: 6px; color: var(--mint-bright); background: rgba(7, 16, 21, .62); font-family: "Cascadia Code", "Consolas", monospace; font-size: .7rem; line-height: 1.5; white-space: pre-wrap; }
|
|
186
213
|
.footer-note { display: flex; justify-content: center; gap: 8px; padding: 18px 0 28px; color: var(--faint); font-size: .7rem; }
|
|
187
214
|
.footer-separator { color: var(--line-strong); }
|
|
188
215
|
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
|
|
@@ -196,7 +223,7 @@ input::placeholder { color: var(--faint); }
|
|
|
196
223
|
.detail-panel { min-height: 420px; }
|
|
197
224
|
.workspace-layout { grid-template-columns: 1fr; }
|
|
198
225
|
.workspace-rail { order: 0; }
|
|
199
|
-
.workspace-nav { grid-template-columns: repeat(
|
|
226
|
+
.workspace-nav { grid-template-columns: repeat(6, 1fr); }
|
|
200
227
|
.workspace-nav a { border-left: 0; border-bottom: 2px solid transparent; padding: 9px 6px; text-align: center; }
|
|
201
228
|
.workspace-nav a:hover, .workspace-nav a.is-active { border-bottom-color: var(--amber); border-left-color: transparent; }
|
|
202
229
|
.workspace-meta { display: none; }
|
|
@@ -212,12 +239,18 @@ input::placeholder { color: var(--faint); }
|
|
|
212
239
|
.panel-heading { flex-direction: column; }
|
|
213
240
|
.search-field, .search-field input { width: 100%; }
|
|
214
241
|
.detail-facts { grid-template-columns: 1fr; }
|
|
242
|
+
.usage-summary-grid { grid-template-columns: repeat(2, 1fr); }
|
|
243
|
+
.usage-token-grid { grid-template-columns: repeat(3, 1fr); }
|
|
215
244
|
.detail-header { flex-direction: column; }
|
|
216
245
|
.workspace-header { grid-template-columns: 1fr auto; align-items: start; gap: 13px; }
|
|
217
246
|
.workspace-header .back-link { grid-column: 1 / -1; }
|
|
218
247
|
.workspace-header h1 { font-size: 2.8rem; }
|
|
219
248
|
.workspace-content { padding: 20px 15px; }
|
|
220
249
|
.workspace-nav { grid-template-columns: repeat(3, 1fr); }
|
|
250
|
+
.usage-filters { align-items: stretch; flex-direction: column; }
|
|
251
|
+
.usage-filter, .usage-filter input, .usage-filter select, .button-primary { width: 100%; }
|
|
252
|
+
.usage-summary-grid { grid-template-columns: 1fr; }
|
|
253
|
+
.usage-token-grid { grid-template-columns: repeat(2, 1fr); }
|
|
221
254
|
.memory-document-row { grid-template-columns: 28px minmax(0, 1fr); }
|
|
222
255
|
.memory-document-size { display: none; }
|
|
223
256
|
.footer-note { flex-direction: column; align-items: center; gap: 4px; }
|