wyrm-mcp 3.7.2 → 4.0.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/dist/audit.d.ts +82 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +151 -0
- package/dist/audit.js.map +1 -0
- package/dist/causality.d.ts +76 -0
- package/dist/causality.d.ts.map +1 -0
- package/dist/causality.js +115 -0
- package/dist/causality.js.map +1 -0
- package/dist/database.d.ts.map +1 -1
- package/dist/database.js +5 -0
- package/dist/database.js.map +1 -1
- package/dist/failure-patterns.d.ts +60 -0
- package/dist/failure-patterns.d.ts.map +1 -0
- package/dist/failure-patterns.js +117 -0
- package/dist/failure-patterns.js.map +1 -0
- package/dist/federation.d.ts +82 -0
- package/dist/federation.d.ts.map +1 -0
- package/dist/federation.js +220 -0
- package/dist/federation.js.map +1 -0
- package/dist/hours.d.ts +82 -0
- package/dist/hours.d.ts.map +1 -0
- package/dist/hours.js +162 -0
- package/dist/hours.js.map +1 -0
- package/dist/http-fast.d.ts.map +1 -1
- package/dist/http-fast.js +183 -1
- package/dist/http-fast.js.map +1 -1
- package/dist/http-server.js +1 -1
- package/dist/http-server.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1000 -99
- package/dist/index.js.map +1 -1
- package/dist/indexer.d.ts +2 -0
- package/dist/indexer.d.ts.map +1 -1
- package/dist/indexer.js +4 -0
- package/dist/indexer.js.map +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +265 -1
- package/dist/migrations.js.map +1 -1
- package/dist/presence.d.ts +71 -0
- package/dist/presence.d.ts.map +1 -0
- package/dist/presence.js +133 -0
- package/dist/presence.js.map +1 -0
- package/dist/rehydration.d.ts +39 -0
- package/dist/rehydration.d.ts.map +1 -0
- package/dist/rehydration.js +178 -0
- package/dist/rehydration.js.map +1 -0
- package/dist/setup.js +0 -0
- package/dist/sub-agent.d.ts +58 -0
- package/dist/sub-agent.d.ts.map +1 -0
- package/dist/sub-agent.js +292 -0
- package/dist/sub-agent.js.map +1 -0
- package/dist/symbols.d.ts +65 -0
- package/dist/symbols.d.ts.map +1 -0
- package/dist/symbols.js +291 -0
- package/dist/symbols.js.map +1 -0
- package/dist/tool-analytics.d.ts +85 -0
- package/dist/tool-analytics.d.ts.map +1 -0
- package/dist/tool-analytics.js +161 -0
- package/dist/tool-analytics.js.map +1 -0
- package/dist/wyrm-cli.js +0 -0
- package/package.json +6 -6
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Failure Patterns — Counter-pattern detection.
|
|
3
|
+
*
|
|
4
|
+
* Records edits / commands / prompts that failed so the predictive push
|
|
5
|
+
* can BLOCK the same suggestion mid-stream the next time it surfaces.
|
|
6
|
+
* Most "memory" tools only learn from success; Wyrm learns from failure too.
|
|
7
|
+
*
|
|
8
|
+
* Keyed on `(signature, scope)` so semantically-identical failures
|
|
9
|
+
* coalesce — re-recording the same failure increments `occurrences`
|
|
10
|
+
* rather than creating a new row.
|
|
11
|
+
*
|
|
12
|
+
* @copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
|
|
13
|
+
* @license Proprietary
|
|
14
|
+
*/
|
|
15
|
+
import type Database from 'better-sqlite3';
|
|
16
|
+
export type FailureScope = 'file' | 'symbol' | 'command' | 'prompt' | 'edit';
|
|
17
|
+
export type FailureSeverity = 'low' | 'medium' | 'high' | 'critical';
|
|
18
|
+
export interface FailurePattern {
|
|
19
|
+
id: number;
|
|
20
|
+
project_id: number | null;
|
|
21
|
+
signature: string;
|
|
22
|
+
scope: FailureScope;
|
|
23
|
+
target: string;
|
|
24
|
+
description: string;
|
|
25
|
+
why_failed: string | null;
|
|
26
|
+
occurrences: number;
|
|
27
|
+
first_seen: string;
|
|
28
|
+
last_seen: string;
|
|
29
|
+
severity: FailureSeverity;
|
|
30
|
+
resolved: 0 | 1;
|
|
31
|
+
resolution_note: string | null;
|
|
32
|
+
}
|
|
33
|
+
export interface RecordInput {
|
|
34
|
+
project_id?: number | null;
|
|
35
|
+
scope: FailureScope;
|
|
36
|
+
target: string;
|
|
37
|
+
description: string;
|
|
38
|
+
why_failed?: string;
|
|
39
|
+
severity?: FailureSeverity;
|
|
40
|
+
}
|
|
41
|
+
export declare class FailurePatterns {
|
|
42
|
+
private db;
|
|
43
|
+
constructor(db: Database.Database);
|
|
44
|
+
/** Build a stable signature from scope+target+description, so repeated
|
|
45
|
+
* identical failures coalesce instead of creating new rows. */
|
|
46
|
+
static signature(scope: string, target: string, description: string): string;
|
|
47
|
+
/** Record (or increment) a failure. Returns the (new or updated) row. */
|
|
48
|
+
record(input: RecordInput): FailurePattern;
|
|
49
|
+
get(id: number): FailurePattern | null;
|
|
50
|
+
/** Check whether a proposed action matches any unresolved failure.
|
|
51
|
+
* Returns the matching patterns, ranked by recency × occurrences. */
|
|
52
|
+
check(scope: FailureScope, target: string, description: string, projectId?: number | null): FailurePattern[];
|
|
53
|
+
/** List unresolved failures in a project, most-recent-first. */
|
|
54
|
+
list(projectId?: number | null, limit?: number): FailurePattern[];
|
|
55
|
+
/** Mark a failure as resolved (the underlying issue was fixed). */
|
|
56
|
+
resolve(id: number, note?: string): FailurePattern | null;
|
|
57
|
+
/** Permanently delete a failure (use sparingly — resolve() is preferred). */
|
|
58
|
+
delete(id: number): boolean;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=failure-patterns.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"failure-patterns.d.ts","sourceRoot":"","sources":["../src/failure-patterns.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAG3C,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,MAAM,CAAC;AAC7E,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU,CAAC;AAErE,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,YAAY,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,eAAe,CAAC;IAC1B,QAAQ,EAAE,CAAC,GAAG,CAAC,CAAC;IAChB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED,MAAM,WAAW,WAAW;IAC1B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,KAAK,EAAE,YAAY,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,eAAe,CAAC;CAC5B;AAED,qBAAa,eAAe;IACd,OAAO,CAAC,EAAE;gBAAF,EAAE,EAAE,QAAQ,CAAC,QAAQ;IAEzC;mEAC+D;IAC/D,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM;IAO5E,yEAAyE;IACzE,MAAM,CAAC,KAAK,EAAE,WAAW,GAAG,cAAc;IA4C1C,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI;IAOtC;yEACqE;IACrE,KAAK,CAAC,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,cAAc,EAAE;IA+B5G,gEAAgE;IAChE,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,EAAE,KAAK,SAAK,GAAG,cAAc,EAAE;IAc7D,mEAAmE;IACnE,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI;IASzD,6EAA6E;IAC7E,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;CAM5B"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Failure Patterns — Counter-pattern detection.
|
|
3
|
+
*
|
|
4
|
+
* Records edits / commands / prompts that failed so the predictive push
|
|
5
|
+
* can BLOCK the same suggestion mid-stream the next time it surfaces.
|
|
6
|
+
* Most "memory" tools only learn from success; Wyrm learns from failure too.
|
|
7
|
+
*
|
|
8
|
+
* Keyed on `(signature, scope)` so semantically-identical failures
|
|
9
|
+
* coalesce — re-recording the same failure increments `occurrences`
|
|
10
|
+
* rather than creating a new row.
|
|
11
|
+
*
|
|
12
|
+
* @copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
|
|
13
|
+
* @license Proprietary
|
|
14
|
+
*/
|
|
15
|
+
import { sanitizeFtsQuery } from './security.js';
|
|
16
|
+
export class FailurePatterns {
|
|
17
|
+
db;
|
|
18
|
+
constructor(db) {
|
|
19
|
+
this.db = db;
|
|
20
|
+
}
|
|
21
|
+
/** Build a stable signature from scope+target+description, so repeated
|
|
22
|
+
* identical failures coalesce instead of creating new rows. */
|
|
23
|
+
static signature(scope, target, description) {
|
|
24
|
+
// Normalize: lowercase, strip whitespace runs, trim
|
|
25
|
+
const norm = (s) => s.toLowerCase().replace(/\s+/g, ' ').trim().slice(0, 200);
|
|
26
|
+
return `${scope}:${norm(target)}:${norm(description).slice(0, 80)}`;
|
|
27
|
+
}
|
|
28
|
+
/** Record (or increment) a failure. Returns the (new or updated) row. */
|
|
29
|
+
record(input) {
|
|
30
|
+
const signature = FailurePatterns.signature(input.scope, input.target, input.description);
|
|
31
|
+
const projectId = input.project_id ?? null;
|
|
32
|
+
const existing = this.db.prepare(`
|
|
33
|
+
SELECT * FROM failure_patterns
|
|
34
|
+
WHERE signature = ? AND (project_id IS ? OR project_id = ?)
|
|
35
|
+
`).get(signature, projectId, projectId);
|
|
36
|
+
if (existing) {
|
|
37
|
+
this.db.prepare(`
|
|
38
|
+
UPDATE failure_patterns
|
|
39
|
+
SET occurrences = occurrences + 1,
|
|
40
|
+
last_seen = datetime('now'),
|
|
41
|
+
why_failed = COALESCE(?, why_failed),
|
|
42
|
+
severity = ?,
|
|
43
|
+
resolved = 0
|
|
44
|
+
WHERE id = ?
|
|
45
|
+
`).run(input.why_failed ?? null, input.severity ?? existing.severity, existing.id);
|
|
46
|
+
return this.get(existing.id);
|
|
47
|
+
}
|
|
48
|
+
const info = this.db.prepare(`
|
|
49
|
+
INSERT INTO failure_patterns
|
|
50
|
+
(project_id, signature, scope, target, description, why_failed, severity)
|
|
51
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
52
|
+
`).run(projectId, signature, input.scope, input.target, input.description, input.why_failed ?? null, input.severity ?? 'medium');
|
|
53
|
+
return this.get(info.lastInsertRowid);
|
|
54
|
+
}
|
|
55
|
+
get(id) {
|
|
56
|
+
const row = this.db.prepare('SELECT * FROM failure_patterns WHERE id = ?').get(id);
|
|
57
|
+
return row ?? null;
|
|
58
|
+
}
|
|
59
|
+
/** Check whether a proposed action matches any unresolved failure.
|
|
60
|
+
* Returns the matching patterns, ranked by recency × occurrences. */
|
|
61
|
+
check(scope, target, description, projectId) {
|
|
62
|
+
const signature = FailurePatterns.signature(scope, target, description);
|
|
63
|
+
const exact = this.db.prepare(`
|
|
64
|
+
SELECT * FROM failure_patterns
|
|
65
|
+
WHERE resolved = 0 AND signature = ?
|
|
66
|
+
AND (project_id IS ? OR project_id = ? OR project_id IS NULL)
|
|
67
|
+
ORDER BY occurrences DESC, last_seen DESC
|
|
68
|
+
LIMIT 5
|
|
69
|
+
`).all(signature, projectId ?? null, projectId ?? null);
|
|
70
|
+
if (exact.length > 0)
|
|
71
|
+
return exact;
|
|
72
|
+
// Fuzzy fallback: FTS over description + target
|
|
73
|
+
const q = sanitizeFtsQuery(`${target} ${description}`.slice(0, 100));
|
|
74
|
+
if (!q)
|
|
75
|
+
return [];
|
|
76
|
+
try {
|
|
77
|
+
return this.db.prepare(`
|
|
78
|
+
SELECT fp.* FROM failure_patterns fp
|
|
79
|
+
JOIN failure_patterns_fts fts ON fts.rowid = fp.id
|
|
80
|
+
WHERE failure_patterns_fts MATCH ?
|
|
81
|
+
AND fp.resolved = 0
|
|
82
|
+
AND (fp.project_id IS ? OR fp.project_id = ? OR fp.project_id IS NULL)
|
|
83
|
+
AND fp.scope = ?
|
|
84
|
+
ORDER BY fp.occurrences DESC, fp.last_seen DESC
|
|
85
|
+
LIMIT 5
|
|
86
|
+
`).all(q, projectId ?? null, projectId ?? null, scope);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** List unresolved failures in a project, most-recent-first. */
|
|
93
|
+
list(projectId, limit = 20) {
|
|
94
|
+
if (projectId == null) {
|
|
95
|
+
return this.db.prepare(`SELECT * FROM failure_patterns WHERE resolved = 0
|
|
96
|
+
ORDER BY last_seen DESC LIMIT ?`).all(limit);
|
|
97
|
+
}
|
|
98
|
+
return this.db.prepare(`SELECT * FROM failure_patterns
|
|
99
|
+
WHERE resolved = 0 AND project_id = ?
|
|
100
|
+
ORDER BY last_seen DESC LIMIT ?`).all(projectId, limit);
|
|
101
|
+
}
|
|
102
|
+
/** Mark a failure as resolved (the underlying issue was fixed). */
|
|
103
|
+
resolve(id, note) {
|
|
104
|
+
this.db.prepare(`
|
|
105
|
+
UPDATE failure_patterns
|
|
106
|
+
SET resolved = 1, resolution_note = ?
|
|
107
|
+
WHERE id = ?
|
|
108
|
+
`).run(note ?? null, id);
|
|
109
|
+
return this.get(id);
|
|
110
|
+
}
|
|
111
|
+
/** Permanently delete a failure (use sparingly — resolve() is preferred). */
|
|
112
|
+
delete(id) {
|
|
113
|
+
const info = this.db.prepare('DELETE FROM failure_patterns WHERE id = ?').run(id);
|
|
114
|
+
return info.changes > 0;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=failure-patterns.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"failure-patterns.js","sourceRoot":"","sources":["../src/failure-patterns.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Federated team Wyrm (Tier 2.8).
|
|
3
|
+
*
|
|
4
|
+
* Each operator runs a local Wyrm; opt-in tagged data syncs to a shared
|
|
5
|
+
* "team Wyrm" instance. PII never leaves the local box unless explicitly
|
|
6
|
+
* shared (per-row `is_shared=1` flag — set when the operator marks a
|
|
7
|
+
* truth/quest/artifact as team-visible).
|
|
8
|
+
*
|
|
9
|
+
* Sync surface:
|
|
10
|
+
* POST /sync/push pushes is_shared=1 local rows changed since cursor
|
|
11
|
+
* GET /sync/pull pulls remote rows changed since cursor
|
|
12
|
+
*
|
|
13
|
+
* Conflict policy: if a remote push lands on top of a local edit whose
|
|
14
|
+
* `updated_at` is newer, we DO NOT silently overwrite. Instead we record
|
|
15
|
+
* the conflict in `sync_conflicts` and mint a quest for the operator to
|
|
16
|
+
* resolve. Last-write-wins is the failure mode of every team sync tool;
|
|
17
|
+
* Wyrm refuses to silently lose work.
|
|
18
|
+
*
|
|
19
|
+
* @copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
|
|
20
|
+
* @license Proprietary
|
|
21
|
+
*/
|
|
22
|
+
import type Database from 'better-sqlite3';
|
|
23
|
+
export type SyncableKind = 'session' | 'quest' | 'truth' | 'artifact' | 'edge';
|
|
24
|
+
export interface SyncableRow {
|
|
25
|
+
kind: SyncableKind;
|
|
26
|
+
id: number;
|
|
27
|
+
project_id?: number | null;
|
|
28
|
+
payload: Record<string, unknown>;
|
|
29
|
+
updated_at: string;
|
|
30
|
+
}
|
|
31
|
+
export interface SyncPushResult {
|
|
32
|
+
pushed: number;
|
|
33
|
+
conflicts: number;
|
|
34
|
+
new_cursor: string;
|
|
35
|
+
conflict_ids: number[];
|
|
36
|
+
}
|
|
37
|
+
export interface SyncPullResult {
|
|
38
|
+
pulled: number;
|
|
39
|
+
new_cursor: string;
|
|
40
|
+
by_kind: Record<string, number>;
|
|
41
|
+
}
|
|
42
|
+
export declare class Federation {
|
|
43
|
+
private db;
|
|
44
|
+
constructor(db: Database.Database);
|
|
45
|
+
/** Mark a row as shared (will be picked up on next push). */
|
|
46
|
+
share(kind: SyncableKind, id: number): boolean;
|
|
47
|
+
/** Unshare (revoke from team visibility). Local row stays. */
|
|
48
|
+
unshare(kind: SyncableKind, id: number): boolean;
|
|
49
|
+
/** Collect rows to push: is_shared=1 AND ts > since_cursor. */
|
|
50
|
+
collectForPush(since_cursor?: string, limit?: number): SyncableRow[];
|
|
51
|
+
/** Apply a remote row locally. Returns:
|
|
52
|
+
* { kind: 'inserted' } — fresh row, no conflict
|
|
53
|
+
* { kind: 'overwritten' } — local row was older, remote wins
|
|
54
|
+
* { kind: 'conflict', conflict_id } — local row newer, conflict recorded
|
|
55
|
+
* Idempotent — applying the same payload twice is a no-op.
|
|
56
|
+
*
|
|
57
|
+
* Conflict policy: if local updated_at > remote updated_at, the remote
|
|
58
|
+
* is REJECTED into sync_conflicts; a quest is minted. The local row stays.
|
|
59
|
+
*/
|
|
60
|
+
applyRemote(row: SyncableRow, remoteOrigin: string): {
|
|
61
|
+
kind: 'inserted' | 'overwritten' | 'conflict' | 'skipped';
|
|
62
|
+
conflict_id?: number;
|
|
63
|
+
};
|
|
64
|
+
/** Bulk push — record one sync_log row, return summary. Caller does HTTP. */
|
|
65
|
+
recordPush(rows: SyncableRow[], remoteOrigin: string, duration_ms: number, since_cursor?: string): SyncPushResult;
|
|
66
|
+
/** Bulk pull — apply each remote row, record sync_log, return summary. */
|
|
67
|
+
applyPull(rows: SyncableRow[], remoteOrigin: string, duration_ms: number, since_cursor?: string): SyncPullResult;
|
|
68
|
+
/** Get the latest cursor from a previous sync. */
|
|
69
|
+
latestCursor(direction: 'push' | 'pull', remoteOrigin: string): string | null;
|
|
70
|
+
/** List unresolved sync conflicts. */
|
|
71
|
+
unresolvedConflicts(limit?: number): Array<{
|
|
72
|
+
id: number;
|
|
73
|
+
local_kind: string;
|
|
74
|
+
local_id: number;
|
|
75
|
+
remote_origin: string | null;
|
|
76
|
+
detected_at: string;
|
|
77
|
+
quest_id: number | null;
|
|
78
|
+
}>;
|
|
79
|
+
/** Resolve a conflict. mode: 'kept_local' | 'kept_remote' | 'merged'. */
|
|
80
|
+
resolveConflict(conflict_id: number, mode: 'kept_local' | 'kept_remote' | 'merged', merged_payload?: Record<string, unknown>): boolean;
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=federation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"federation.d.ts","sourceRoot":"","sources":["../src/federation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAE3C,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,OAAO,GAAG,OAAO,GAAG,UAAU,GAAG,MAAM,CAAC;AAE/E,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,YAAY,CAAC;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAUD,qBAAa,UAAU;IACT,OAAO,CAAC,EAAE;gBAAF,EAAE,EAAE,QAAQ,CAAC,QAAQ;IAEzC,6DAA6D;IAC7D,KAAK,CAAC,IAAI,EAAE,YAAY,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO;IAS9C,8DAA8D;IAC9D,OAAO,CAAC,IAAI,EAAE,YAAY,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO;IAShD,+DAA+D;IAC/D,cAAc,CAAC,YAAY,CAAC,EAAE,MAAM,EAAE,KAAK,SAAM,GAAG,WAAW,EAAE;IA2BjE;;;;;;;;OAQG;IACH,WAAW,CAAC,GAAG,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,GAAG;QAAE,IAAI,EAAE,UAAU,GAAG,aAAa,GAAG,UAAU,GAAG,SAAS,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE;IAsExI,6EAA6E;IAC7E,UAAU,CAAC,IAAI,EAAE,WAAW,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,cAAc;IAgBjH,0EAA0E;IAC1E,SAAS,CAAC,IAAI,EAAE,WAAW,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,cAAc;IAoBhH,kDAAkD;IAClD,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAS7E,sCAAsC;IACtC,mBAAmB,CAAC,KAAK,SAAK,GAAG,KAAK,CAAC;QACrC,EAAE,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QACjD,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;KAC5E,CAAC;IAYF,yEAAyE;IACzE,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,GAAG,aAAa,GAAG,QAAQ,EAAE,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO;CAmCvI"}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Federated team Wyrm (Tier 2.8).
|
|
3
|
+
*
|
|
4
|
+
* Each operator runs a local Wyrm; opt-in tagged data syncs to a shared
|
|
5
|
+
* "team Wyrm" instance. PII never leaves the local box unless explicitly
|
|
6
|
+
* shared (per-row `is_shared=1` flag — set when the operator marks a
|
|
7
|
+
* truth/quest/artifact as team-visible).
|
|
8
|
+
*
|
|
9
|
+
* Sync surface:
|
|
10
|
+
* POST /sync/push pushes is_shared=1 local rows changed since cursor
|
|
11
|
+
* GET /sync/pull pulls remote rows changed since cursor
|
|
12
|
+
*
|
|
13
|
+
* Conflict policy: if a remote push lands on top of a local edit whose
|
|
14
|
+
* `updated_at` is newer, we DO NOT silently overwrite. Instead we record
|
|
15
|
+
* the conflict in `sync_conflicts` and mint a quest for the operator to
|
|
16
|
+
* resolve. Last-write-wins is the failure mode of every team sync tool;
|
|
17
|
+
* Wyrm refuses to silently lose work.
|
|
18
|
+
*
|
|
19
|
+
* @copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
|
|
20
|
+
* @license Proprietary
|
|
21
|
+
*/
|
|
22
|
+
const KIND_TO_TABLE = {
|
|
23
|
+
session: { table: 'sessions', ts: 'created_at' },
|
|
24
|
+
quest: { table: 'quests', ts: 'created_at' },
|
|
25
|
+
truth: { table: 'ground_truths', ts: 'updated_at' },
|
|
26
|
+
artifact: { table: 'memory_artifacts', ts: 'last_accessed_at' },
|
|
27
|
+
edge: { table: 'decision_edges', ts: 'created_at' },
|
|
28
|
+
};
|
|
29
|
+
export class Federation {
|
|
30
|
+
db;
|
|
31
|
+
constructor(db) {
|
|
32
|
+
this.db = db;
|
|
33
|
+
}
|
|
34
|
+
/** Mark a row as shared (will be picked up on next push). */
|
|
35
|
+
share(kind, id) {
|
|
36
|
+
const t = KIND_TO_TABLE[kind];
|
|
37
|
+
if (!t)
|
|
38
|
+
return false;
|
|
39
|
+
const info = this.db.prepare(`UPDATE ${t.table} SET is_shared = 1 WHERE id = ?`).run(id);
|
|
40
|
+
return info.changes > 0;
|
|
41
|
+
}
|
|
42
|
+
/** Unshare (revoke from team visibility). Local row stays. */
|
|
43
|
+
unshare(kind, id) {
|
|
44
|
+
const t = KIND_TO_TABLE[kind];
|
|
45
|
+
if (!t)
|
|
46
|
+
return false;
|
|
47
|
+
const info = this.db.prepare(`UPDATE ${t.table} SET is_shared = 0 WHERE id = ?`).run(id);
|
|
48
|
+
return info.changes > 0;
|
|
49
|
+
}
|
|
50
|
+
/** Collect rows to push: is_shared=1 AND ts > since_cursor. */
|
|
51
|
+
collectForPush(since_cursor, limit = 500) {
|
|
52
|
+
const cursor = since_cursor ?? '1970-01-01';
|
|
53
|
+
const out = [];
|
|
54
|
+
for (const [kindStr, t] of Object.entries(KIND_TO_TABLE)) {
|
|
55
|
+
if (out.length >= limit)
|
|
56
|
+
break;
|
|
57
|
+
const kind = kindStr;
|
|
58
|
+
const rows = this.db.prepare(`SELECT * FROM ${t.table}
|
|
59
|
+
WHERE is_shared = 1 AND ${t.ts} > ?
|
|
60
|
+
ORDER BY ${t.ts} ASC
|
|
61
|
+
LIMIT ?`).all(cursor, Math.max(1, limit - out.length));
|
|
62
|
+
for (const r of rows) {
|
|
63
|
+
out.push({
|
|
64
|
+
kind,
|
|
65
|
+
id: r.id,
|
|
66
|
+
project_id: (r.project_id ?? null),
|
|
67
|
+
payload: r,
|
|
68
|
+
updated_at: r[t.ts] || new Date().toISOString(),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
/** Apply a remote row locally. Returns:
|
|
75
|
+
* { kind: 'inserted' } — fresh row, no conflict
|
|
76
|
+
* { kind: 'overwritten' } — local row was older, remote wins
|
|
77
|
+
* { kind: 'conflict', conflict_id } — local row newer, conflict recorded
|
|
78
|
+
* Idempotent — applying the same payload twice is a no-op.
|
|
79
|
+
*
|
|
80
|
+
* Conflict policy: if local updated_at > remote updated_at, the remote
|
|
81
|
+
* is REJECTED into sync_conflicts; a quest is minted. The local row stays.
|
|
82
|
+
*/
|
|
83
|
+
applyRemote(row, remoteOrigin) {
|
|
84
|
+
const t = KIND_TO_TABLE[row.kind];
|
|
85
|
+
if (!t)
|
|
86
|
+
return { kind: 'skipped' };
|
|
87
|
+
const localTs = this.db.prepare(`SELECT ${t.ts} as ts FROM ${t.table} WHERE id = ?`).get(row.id);
|
|
88
|
+
if (!localTs) {
|
|
89
|
+
// Fresh insert. Build columns from payload, skipping id (auto-incremented)
|
|
90
|
+
// — but we want to preserve the same id so future syncs are stable.
|
|
91
|
+
// SQLite allows explicit id insert.
|
|
92
|
+
const cols = Object.keys(row.payload).filter(c => c !== 'rowid');
|
|
93
|
+
const placeholders = cols.map(() => '?').join(',');
|
|
94
|
+
const values = cols.map(c => row.payload[c]);
|
|
95
|
+
try {
|
|
96
|
+
this.db.prepare(`INSERT OR IGNORE INTO ${t.table} (${cols.join(',')}) VALUES (${placeholders})`).run(...values);
|
|
97
|
+
return { kind: 'inserted' };
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return { kind: 'skipped' };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Existing row — compare timestamps.
|
|
104
|
+
const localNewer = localTs.ts > row.updated_at;
|
|
105
|
+
if (localNewer) {
|
|
106
|
+
// Conflict — record it and mint a quest if this row has a project_id.
|
|
107
|
+
const conflictInfo = this.db.prepare(`
|
|
108
|
+
INSERT INTO sync_conflicts
|
|
109
|
+
(local_kind, local_id, remote_payload, remote_origin)
|
|
110
|
+
VALUES (?, ?, ?, ?)
|
|
111
|
+
`).run(row.kind, row.id, JSON.stringify(row.payload), remoteOrigin);
|
|
112
|
+
const conflict_id = conflictInfo.lastInsertRowid;
|
|
113
|
+
// Best-effort: mint a quest so the operator sees it on next session
|
|
114
|
+
if (row.project_id != null) {
|
|
115
|
+
try {
|
|
116
|
+
this.db.prepare(`
|
|
117
|
+
INSERT INTO quests (project_id, title, description, priority, status, tags)
|
|
118
|
+
VALUES (?, ?, ?, 'high', 'pending', 'sync-conflict')
|
|
119
|
+
`).run(row.project_id, `[sync] Resolve conflict on ${row.kind} #${row.id}`, `A remote ${remoteOrigin} pushed a ${row.kind} that conflicts with a newer local edit. ` +
|
|
120
|
+
`See sync_conflicts row #${conflict_id} for the remote payload. ` +
|
|
121
|
+
`Resolve with wyrm_sync_resolve (kept_local | kept_remote | merged).`);
|
|
122
|
+
}
|
|
123
|
+
catch { /* best-effort */ }
|
|
124
|
+
}
|
|
125
|
+
return { kind: 'conflict', conflict_id };
|
|
126
|
+
}
|
|
127
|
+
// Local is older — accept remote overwrite.
|
|
128
|
+
const cols = Object.keys(row.payload).filter(c => c !== 'id');
|
|
129
|
+
const setClause = cols.map(c => `${c} = ?`).join(', ');
|
|
130
|
+
const values = cols.map(c => row.payload[c]);
|
|
131
|
+
try {
|
|
132
|
+
this.db.prepare(`UPDATE ${t.table} SET ${setClause} WHERE id = ?`).run(...values, row.id);
|
|
133
|
+
return { kind: 'overwritten' };
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return { kind: 'skipped' };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/** Bulk push — record one sync_log row, return summary. Caller does HTTP. */
|
|
140
|
+
recordPush(rows, remoteOrigin, duration_ms, since_cursor) {
|
|
141
|
+
const newCursor = new Date().toISOString();
|
|
142
|
+
this.db.prepare(`
|
|
143
|
+
INSERT INTO sync_log
|
|
144
|
+
(direction, remote_origin, kind, count, since_cursor, new_cursor, duration_ms)
|
|
145
|
+
VALUES ('push', ?, 'mixed', ?, ?, ?, ?)
|
|
146
|
+
`).run(remoteOrigin, rows.length, since_cursor ?? null, newCursor, duration_ms);
|
|
147
|
+
return {
|
|
148
|
+
pushed: rows.length,
|
|
149
|
+
conflicts: 0,
|
|
150
|
+
new_cursor: newCursor,
|
|
151
|
+
conflict_ids: [],
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/** Bulk pull — apply each remote row, record sync_log, return summary. */
|
|
155
|
+
applyPull(rows, remoteOrigin, duration_ms, since_cursor) {
|
|
156
|
+
const byKind = {};
|
|
157
|
+
let pulled = 0;
|
|
158
|
+
for (const row of rows) {
|
|
159
|
+
const result = this.applyRemote(row, remoteOrigin);
|
|
160
|
+
if (result.kind === 'inserted' || result.kind === 'overwritten') {
|
|
161
|
+
pulled++;
|
|
162
|
+
byKind[row.kind] = (byKind[row.kind] ?? 0) + 1;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const newCursor = new Date().toISOString();
|
|
166
|
+
this.db.prepare(`
|
|
167
|
+
INSERT INTO sync_log
|
|
168
|
+
(direction, remote_origin, kind, count, since_cursor, new_cursor, duration_ms)
|
|
169
|
+
VALUES ('pull', ?, 'mixed', ?, ?, ?, ?)
|
|
170
|
+
`).run(remoteOrigin, pulled, since_cursor ?? null, newCursor, duration_ms);
|
|
171
|
+
return { pulled, new_cursor: newCursor, by_kind: byKind };
|
|
172
|
+
}
|
|
173
|
+
/** Get the latest cursor from a previous sync. */
|
|
174
|
+
latestCursor(direction, remoteOrigin) {
|
|
175
|
+
const row = this.db.prepare(`
|
|
176
|
+
SELECT new_cursor FROM sync_log
|
|
177
|
+
WHERE direction = ? AND remote_origin = ?
|
|
178
|
+
ORDER BY id DESC LIMIT 1
|
|
179
|
+
`).get(direction, remoteOrigin);
|
|
180
|
+
return row?.new_cursor ?? null;
|
|
181
|
+
}
|
|
182
|
+
/** List unresolved sync conflicts. */
|
|
183
|
+
unresolvedConflicts(limit = 50) {
|
|
184
|
+
return this.db.prepare(`
|
|
185
|
+
SELECT id, local_kind, local_id, remote_origin, detected_at, quest_id
|
|
186
|
+
FROM sync_conflicts
|
|
187
|
+
WHERE resolved_at IS NULL
|
|
188
|
+
ORDER BY detected_at DESC LIMIT ?
|
|
189
|
+
`).all(limit);
|
|
190
|
+
}
|
|
191
|
+
/** Resolve a conflict. mode: 'kept_local' | 'kept_remote' | 'merged'. */
|
|
192
|
+
resolveConflict(conflict_id, mode, merged_payload) {
|
|
193
|
+
const conflict = this.db.prepare('SELECT * FROM sync_conflicts WHERE id = ? AND resolved_at IS NULL').get(conflict_id);
|
|
194
|
+
if (!conflict)
|
|
195
|
+
return false;
|
|
196
|
+
if (mode === 'kept_remote' || mode === 'merged') {
|
|
197
|
+
const t = KIND_TO_TABLE[conflict.local_kind];
|
|
198
|
+
if (t) {
|
|
199
|
+
const payload = mode === 'merged' && merged_payload
|
|
200
|
+
? merged_payload
|
|
201
|
+
: JSON.parse(conflict.remote_payload);
|
|
202
|
+
const cols = Object.keys(payload).filter(c => c !== 'id');
|
|
203
|
+
const setClause = cols.map(c => `${c} = ?`).join(', ');
|
|
204
|
+
const values = cols.map(c => payload[c]);
|
|
205
|
+
try {
|
|
206
|
+
this.db.prepare(`UPDATE ${t.table} SET ${setClause} WHERE id = ?`).run(...values, conflict.local_id);
|
|
207
|
+
}
|
|
208
|
+
catch { /* best-effort */ }
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// 'kept_local' = no row mutation needed.
|
|
212
|
+
this.db.prepare(`
|
|
213
|
+
UPDATE sync_conflicts
|
|
214
|
+
SET resolved_at = datetime('now'), resolution = ?
|
|
215
|
+
WHERE id = ?
|
|
216
|
+
`).run(mode, conflict_id);
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
//# sourceMappingURL=federation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"federation.js","sourceRoot":"","sources":["../src/federation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AA2BH,MAAM,aAAa,GAAwD;IACzE,OAAO,EAAG,EAAE,KAAK,EAAE,UAAU,EAAU,EAAE,EAAE,YAAY,EAAE;IACzD,KAAK,EAAK,EAAE,KAAK,EAAE,QAAQ,EAAY,EAAE,EAAE,YAAY,EAAE;IACzD,KAAK,EAAK,EAAE,KAAK,EAAE,eAAe,EAAK,EAAE,EAAE,YAAY,EAAE;IACzD,QAAQ,EAAE,EAAE,KAAK,EAAE,kBAAkB,EAAE,EAAE,EAAE,kBAAkB,EAAE;IAC/D,IAAI,EAAM,EAAE,KAAK,EAAE,gBAAgB,EAAI,EAAE,EAAE,YAAY,EAAE;CAC1D,CAAC;AAEF,MAAM,OAAO,UAAU;IACD;IAApB,YAAoB,EAAqB;QAArB,OAAE,GAAF,EAAE,CAAmB;IAAG,CAAC;IAE7C,6DAA6D;IAC7D,KAAK,CAAC,IAAkB,EAAE,EAAU;QAClC,MAAM,CAAC,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;QAC9B,IAAI,CAAC,CAAC;YAAE,OAAO,KAAK,CAAC;QACrB,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC,OAAO,CAC1B,UAAU,CAAC,CAAC,KAAK,iCAAiC,CACnD,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACV,OAAO,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC;IAC1B,CAAC;IAED,8DAA8D;IAC9D,OAAO,CAAC,IAAkB,EAAE,EAAU;QACpC,MAAM,CAAC,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;QAC9B,IAAI,CAAC,CAAC;YAAE,OAAO,KAAK,CAAC;QACrB,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC,OAAO,CAC1B,UAAU,CAAC,CAAC,KAAK,iCAAiC,CACnD,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACV,OAAO,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC;IAC1B,CAAC;IAED,+DAA+D;IAC/D,cAAc,CAAC,YAAqB,EAAE,KAAK,GAAG,GAAG;QAC/C,MAAM,MAAM,GAAG,YAAY,IAAI,YAAY,CAAC;QAC5C,MAAM,GAAG,GAAkB,EAAE,CAAC;QAE9B,KAAK,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,aAAa,CAAC,EAAE,CAAC;YACzD,IAAI,GAAG,CAAC,MAAM,IAAI,KAAK;gBAAE,MAAM;YAC/B,MAAM,IAAI,GAAG,OAAuB,CAAC;YACrC,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC,OAAO,CAC1B,iBAAiB,CAAC,CAAC,KAAK;mCACG,CAAC,CAAC,EAAE;oBACnB,CAAC,CAAC,EAAE;iBACP,CACV,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,GAAG,CAAC,MAAM,CAAC,CAAmC,CAAC;YAEjF,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;gBACrB,GAAG,CAAC,IAAI,CAAC;oBACP,IAAI;oBACJ,EAAE,EAAE,CAAC,CAAC,EAAY;oBAClB,UAAU,EAAE,CAAC,CAAC,CAAC,UAAU,IAAI,IAAI,CAAkB;oBACnD,OAAO,EAAE,CAAC;oBACV,UAAU,EAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAY,IAAI,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;iBAC5D,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC;IAED;;;;;;;;OAQG;IACH,WAAW,CAAC,GAAgB,EAAE,YAAoB;QAChD,MAAM,CAAC,GAAG,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAClC,IAAI,CAAC,CAAC;YAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;QAEnC,MAAM,OAAO,GAAG,IAAI,CAAC,EAAE,CAAC,OAAO,CAC7B,UAAU,CAAC,CAAC,EAAE,eAAe,CAAC,CAAC,KAAK,eAAe,CACpD,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAA+B,CAAC;QAE5C,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,2EAA2E;YAC3E,oEAAoE;YACpE,oCAAoC;YACpC,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC;YACjE,MAAM,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACnD,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAE,GAAG,CAAC,OAAmC,CAAC,CAAC,CAAC,CAAC,CAAC;YAC1E,IAAI,CAAC;gBACH,IAAI,CAAC,EAAE,CAAC,OAAO,CACb,yBAAyB,CAAC,CAAC,KAAK,KAAK,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,aAAa,YAAY,GAAG,CAChF,CAAC,GAAG,CAAC,GAAG,MAAmB,CAAC,CAAC;gBAC9B,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;YAC9B,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;YAC7B,CAAC;QACH,CAAC;QAED,qCAAqC;QACrC,MAAM,UAAU,GAAG,OAAO,CAAC,EAAE,GAAG,GAAG,CAAC,UAAU,CAAC;QAC/C,IAAI,UAAU,EAAE,CAAC;YACf,sEAAsE;YACtE,MAAM,YAAY,GAAG,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC;;;;OAIpC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,YAAY,CAAC,CAAC;YAEpE,MAAM,WAAW,GAAG,YAAY,CAAC,eAAyB,CAAC;YAE3D,oEAAoE;YACpE,IAAI,GAAG,CAAC,UAAU,IAAI,IAAI,EAAE,CAAC;gBAC3B,IAAI,CAAC;oBACH,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC;;;WAGf,CAAC,CAAC,GAAG,CACJ,GAAG,CAAC,UAAU,EACd,8BAA8B,GAAG,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,EAAE,EACnD,YAAY,YAAY,aAAa,GAAG,CAAC,IAAI,2CAA2C;wBACxF,2BAA2B,WAAW,2BAA2B;wBACjE,qEAAqE,CACtE,CAAC;gBACJ,CAAC;gBAAC,MAAM,CAAC,CAAC,iBAAiB,CAAC,CAAC;YAC/B,CAAC;YAED,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,CAAC;QAC3C,CAAC;QAED,4CAA4C;QAC5C,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;QAC9D,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACvD,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAE,GAAG,CAAC,OAAmC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC1E,IAAI,CAAC;YACH,IAAI,CAAC,EAAE,CAAC,OAAO,CACb,UAAU,CAAC,CAAC,KAAK,QAAQ,SAAS,eAAe,CAClD,CAAC,GAAG,CAAC,GAAG,MAAmB,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;YACtC,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC;QACjC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;QAC7B,CAAC;IACH,CAAC;IAED,6EAA6E;IAC7E,UAAU,CAAC,IAAmB,EAAE,YAAoB,EAAE,WAAmB,EAAE,YAAqB;QAC9F,MAAM,SAAS,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QAC3C,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC;;;;KAIf,CAAC,CAAC,GAAG,CAAC,YAAY,EAAE,IAAI,CAAC,MAAM,EAAE,YAAY,IAAI,IAAI,EAAE,SAAS,EAAE,WAAW,CAAC,CAAC;QAEhF,OAAO;YACL,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,SAAS,EAAE,CAAC;YACZ,UAAU,EAAE,SAAS;YACrB,YAAY,EAAE,EAAE;SACjB,CAAC;IACJ,CAAC;IAED,0EAA0E;IAC1E,SAAS,CAAC,IAAmB,EAAE,YAAoB,EAAE,WAAmB,EAAE,YAAqB;QAC7F,MAAM,MAAM,GAA2B,EAAE,CAAC;QAC1C,IAAI,MAAM,GAAG,CAAC,CAAC;QACf,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;YACvB,MAAM,MAAM,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;YACnD,IAAI,MAAM,CAAC,IAAI,KAAK,UAAU,IAAI,MAAM,CAAC,IAAI,KAAK,aAAa,EAAE,CAAC;gBAChE,MAAM,EAAE,CAAC;gBACT,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;YACjD,CAAC;QACH,CAAC;QACD,MAAM,SAAS,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QAC3C,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC;;;;KAIf,CAAC,CAAC,GAAG,CAAC,YAAY,EAAE,MAAM,EAAE,YAAY,IAAI,IAAI,EAAE,SAAS,EAAE,WAAW,CAAC,CAAC;QAE3E,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IAC5D,CAAC;IAED,kDAAkD;IAClD,YAAY,CAAC,SAA0B,EAAE,YAAoB;QAC3D,MAAM,GAAG,GAAG,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC;;;;KAI3B,CAAC,CAAC,GAAG,CAAC,SAAS,EAAE,YAAY,CAAuC,CAAC;QACtE,OAAO,GAAG,EAAE,UAAU,IAAI,IAAI,CAAC;IACjC,CAAC;IAED,sCAAsC;IACtC,mBAAmB,CAAC,KAAK,GAAG,EAAE;QAI5B,OAAO,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC;;;;;KAKtB,CAAC,CAAC,GAAG,CAAC,KAAK,CAGV,CAAC;IACL,CAAC;IAED,yEAAyE;IACzE,eAAe,CAAC,WAAmB,EAAE,IAA6C,EAAE,cAAwC;QAC1H,MAAM,QAAQ,GAAG,IAAI,CAAC,EAAE,CAAC,OAAO,CAC9B,mEAAmE,CACpE,CAAC,GAAG,CAAC,WAAW,CAGJ,CAAC;QACd,IAAI,CAAC,QAAQ;YAAE,OAAO,KAAK,CAAC;QAE5B,IAAI,IAAI,KAAK,aAAa,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;YAChD,MAAM,CAAC,GAAG,aAAa,CAAC,QAAQ,CAAC,UAA0B,CAAC,CAAC;YAC7D,IAAI,CAAC,EAAE,CAAC;gBACN,MAAM,OAAO,GAAG,IAAI,KAAK,QAAQ,IAAI,cAAc;oBACjD,CAAC,CAAC,cAAc;oBAChB,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,cAAc,CAA4B,CAAC;gBACnE,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;gBAC1D,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACvD,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;gBACzC,IAAI,CAAC;oBACH,IAAI,CAAC,EAAE,CAAC,OAAO,CACb,UAAU,CAAC,CAAC,KAAK,QAAQ,SAAS,eAAe,CAClD,CAAC,GAAG,CAAC,GAAG,MAAmB,EAAE,QAAQ,CAAC,QAAQ,CAAC,CAAC;gBACnD,CAAC;gBAAC,MAAM,CAAC,CAAC,iBAAiB,CAAC,CAAC;YAC/B,CAAC;QACH,CAAC;QACD,yCAAyC;QAEzC,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC;;;;KAIf,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;QAE1B,OAAO,IAAI,CAAC;IACd,CAAC;CACF"}
|
package/dist/hours.d.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hour Ledger + Invoice Generator.
|
|
3
|
+
*
|
|
4
|
+
* Sessions already record start/end timestamps. This module derives:
|
|
5
|
+
* - hours per project / per client / per date range
|
|
6
|
+
* - markdown invoices with per-session line items
|
|
7
|
+
*
|
|
8
|
+
* Solo founders / freelancers use Wyrm for memory; this turns it into
|
|
9
|
+
* their time tracker too — no separate Toggl/Harvest needed.
|
|
10
|
+
*
|
|
11
|
+
* Session duration falls back to a configurable default (e.g. 60 min)
|
|
12
|
+
* when the session has no explicit end_time — common for "I started
|
|
13
|
+
* a session and forgot to close it". The fallback is exposed in the
|
|
14
|
+
* report so the operator can review and correct.
|
|
15
|
+
*
|
|
16
|
+
* @copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
|
|
17
|
+
* @license Proprietary
|
|
18
|
+
*/
|
|
19
|
+
import type Database from 'better-sqlite3';
|
|
20
|
+
export interface SessionRow {
|
|
21
|
+
id: number;
|
|
22
|
+
project_id: number;
|
|
23
|
+
date: string;
|
|
24
|
+
objectives: string | null;
|
|
25
|
+
completed: string | null;
|
|
26
|
+
notes: string | null;
|
|
27
|
+
summary: string | null;
|
|
28
|
+
created_at: string;
|
|
29
|
+
}
|
|
30
|
+
export interface HourReportEntry {
|
|
31
|
+
session_id: number;
|
|
32
|
+
project_id: number;
|
|
33
|
+
project_name: string;
|
|
34
|
+
date: string;
|
|
35
|
+
hours: number;
|
|
36
|
+
is_estimated: boolean;
|
|
37
|
+
summary: string | null;
|
|
38
|
+
}
|
|
39
|
+
export interface HourReport {
|
|
40
|
+
range: {
|
|
41
|
+
start: string;
|
|
42
|
+
end: string;
|
|
43
|
+
};
|
|
44
|
+
total_hours: number;
|
|
45
|
+
estimated_sessions: number;
|
|
46
|
+
by_project: Array<{
|
|
47
|
+
project_id: number;
|
|
48
|
+
project_name: string;
|
|
49
|
+
hours: number;
|
|
50
|
+
session_count: number;
|
|
51
|
+
}>;
|
|
52
|
+
entries: HourReportEntry[];
|
|
53
|
+
}
|
|
54
|
+
export interface InvoiceInput {
|
|
55
|
+
client_name: string;
|
|
56
|
+
client_address?: string;
|
|
57
|
+
business_name?: string;
|
|
58
|
+
business_address?: string;
|
|
59
|
+
business_contact?: string;
|
|
60
|
+
invoice_number?: string;
|
|
61
|
+
hourly_rate_usd: number;
|
|
62
|
+
range_start: string;
|
|
63
|
+
range_end: string;
|
|
64
|
+
project_id?: number;
|
|
65
|
+
default_session_hours?: number;
|
|
66
|
+
currency?: string;
|
|
67
|
+
notes?: string;
|
|
68
|
+
}
|
|
69
|
+
export declare class HourLedger {
|
|
70
|
+
private db;
|
|
71
|
+
constructor(db: Database.Database);
|
|
72
|
+
/** Hours report for a date range, optionally scoped to a project. */
|
|
73
|
+
report(opts: {
|
|
74
|
+
range_start: string;
|
|
75
|
+
range_end: string;
|
|
76
|
+
project_id?: number;
|
|
77
|
+
default_session_hours?: number;
|
|
78
|
+
}): HourReport;
|
|
79
|
+
/** Generate a markdown invoice from an hour report. */
|
|
80
|
+
invoice(input: InvoiceInput): string;
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=hours.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hours.d.ts","sourceRoot":"","sources":["../src/hours.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAE3C,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,OAAO,CAAC;IACtB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IACtC,WAAW,EAAE,MAAM,CAAC;IACpB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,UAAU,EAAE,KAAK,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACtG,OAAO,EAAE,eAAe,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,qBAAa,UAAU;IACT,OAAO,CAAC,EAAE;gBAAF,EAAE,EAAE,QAAQ,CAAC,QAAQ;IAEzC,qEAAqE;IACrE,MAAM,CAAC,IAAI,EAAE;QACX,WAAW,EAAE,MAAM,CAAC;QACpB,SAAS,EAAE,MAAM,CAAC;QAClB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,qBAAqB,CAAC,EAAE,MAAM,CAAC;KAChC,GAAG,UAAU;IA8Ed,uDAAuD;IACvD,OAAO,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM;CAsErC"}
|