@rikaidev/yomi 0.5.0 → 0.6.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/README.md +28 -0
- package/dist/ambient/ambient-event-graph.js +172 -0
- package/dist/ambient/ambient-observer.js +71 -0
- package/dist/ambient/event-graph-reducer.js +300 -0
- package/dist/ambient/ingestion-adapter.js +203 -0
- package/dist/ambient/journal/crypto.js +58 -0
- package/dist/ambient/journal/journal-store.js +284 -0
- package/dist/ambient/journal/types.js +7 -0
- package/dist/ambient/summaries/retriever.js +55 -0
- package/dist/ambient/summaries/summary-engine.js +128 -0
- package/dist/ambient/summaries/types.js +4 -0
- package/dist/attention/policy.js +22 -16
- package/dist/auth/credential-store.js +15 -6
- package/dist/cli/query.js +276 -69
- package/dist/cli/work-item.js +24 -0
- package/dist/line/client/auth-service/client.js +7 -8
- package/dist/line/client/buddy-service/client.js +73 -0
- package/dist/line/client/index.js +28 -1
- package/dist/line/client/obs-media-client.js +2 -1
- package/dist/line/client/square-service/client.js +298 -0
- package/dist/line/client/talk-service/command-client.js +9 -1
- package/dist/line/client/talk-service/message-id-query.js +27 -3
- package/dist/line/client/talk-service/requests.js +9 -0
- package/dist/line/core/auth-session-runtime.js +1 -1
- package/dist/line/core/chat-event.js +2 -0
- package/dist/line/core/chat-runtime-service.js +18 -3
- package/dist/line/core/config.js +1 -0
- package/dist/line/core/flex-message.js +323 -0
- package/dist/line/core/service.js +0 -1
- package/dist/mcp/handlers/contacts.js +12 -6
- package/dist/mcp/handlers/conversations.js +54 -0
- package/dist/mcp/handlers/send.js +8 -11
- package/dist/mcp/media.js +101 -2
- package/dist/mcp/names.js +78 -6
- package/dist/search/default-embedder.js +12 -3
- package/dist/version.js +1 -1
- package/dist/work-item/adapter.js +1 -0
- package/dist/work-item/destination-contracts.js +1 -0
- package/dist/work-item/event-context.js +73 -0
- package/dist/work-item/gold-contract.js +101 -0
- package/dist/work-item/gold-coverage.js +96 -0
- package/dist/work-item/gold-derivation.js +67 -0
- package/dist/work-item/gold-split.js +143 -0
- package/dist/work-item/index.js +23 -0
- package/dist/work-item/line-adapter.js +111 -0
- package/dist/work-item/llama-cpp-work-event-adapter.js +239 -0
- package/dist/work-item/local-provider.js +38 -0
- package/dist/work-item/local-work-event-provider.js +56 -0
- package/dist/work-item/onnx-work-event-adapter.js +223 -0
- package/dist/work-item/provider.js +21 -0
- package/dist/work-item/registry.js +19 -0
- package/dist/work-item/service.js +85 -0
- package/dist/work-item/store.js +97 -0
- package/dist/work-item/types.js +79 -0
- package/dist/work-item/work-event-graph-engine.js +63 -0
- package/dist/work-item/work-event-inference.js +267 -0
- package/dist/work-item/work-event-matcher.js +105 -0
- package/dist/work-item/work-event-model-manifest.js +39 -0
- package/dist/work-item/work-event-store.js +125 -0
- package/dist/work-item/work-event.js +123 -0
- package/package.json +24 -3
- package/run.mjs +162 -9
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
function ngrams(value, size = 2) {
|
|
2
|
+
const normalized = value
|
|
3
|
+
.normalize('NFKC')
|
|
4
|
+
.toLocaleLowerCase()
|
|
5
|
+
.replace(/[\p{P}\p{S}\s]+/gu, '');
|
|
6
|
+
if (!normalized)
|
|
7
|
+
return new Set();
|
|
8
|
+
if (normalized.length <= size)
|
|
9
|
+
return new Set([normalized]);
|
|
10
|
+
return new Set(Array.from({ length: normalized.length - size + 1 }, (_, index) => normalized.slice(index, index + size)));
|
|
11
|
+
}
|
|
12
|
+
function jaccard(left, right) {
|
|
13
|
+
if (left.size === 0 && right.size === 0)
|
|
14
|
+
return 1;
|
|
15
|
+
if (left.size === 0 || right.size === 0)
|
|
16
|
+
return 0;
|
|
17
|
+
let intersection = 0;
|
|
18
|
+
for (const value of left)
|
|
19
|
+
if (right.has(value))
|
|
20
|
+
intersection += 1;
|
|
21
|
+
return intersection / (left.size + right.size - intersection);
|
|
22
|
+
}
|
|
23
|
+
function actionText(event) {
|
|
24
|
+
if (!event.action)
|
|
25
|
+
return '';
|
|
26
|
+
return [
|
|
27
|
+
event.action.predicate,
|
|
28
|
+
event.action.object,
|
|
29
|
+
event.action.expectedOutcome,
|
|
30
|
+
]
|
|
31
|
+
.filter(Boolean)
|
|
32
|
+
.join(' ');
|
|
33
|
+
}
|
|
34
|
+
export function workEventSimilarity(previous, incoming) {
|
|
35
|
+
if (previous.chatId !== incoming.chatId ||
|
|
36
|
+
previous.createsWork !== incoming.createsWork)
|
|
37
|
+
return 0;
|
|
38
|
+
const evidence = jaccard(new Set(previous.evidence.map((item) => item.messageId)), new Set(incoming.evidence.map((item) => item.messageId)));
|
|
39
|
+
const action = jaccard(ngrams(actionText(previous)), ngrams(actionText(incoming)));
|
|
40
|
+
const previousOwners = new Set(previous.ownership.owners.map((owner) => owner.kind));
|
|
41
|
+
const incomingOwners = new Set(incoming.ownership.owners.map((owner) => owner.kind));
|
|
42
|
+
const ownership = jaccard(previousOwners, incomingOwners);
|
|
43
|
+
return Number((evidence * 0.45 + action * 0.45 + ownership * 0.1).toFixed(4));
|
|
44
|
+
}
|
|
45
|
+
/** Maximum-weight one-to-one matching keeps sibling events distinct across revisions. */
|
|
46
|
+
export function matchWorkEventRevisions(previous, incoming, minimumScore = 0.55) {
|
|
47
|
+
const candidates = incoming
|
|
48
|
+
.flatMap((next, incomingIndex) => previous.map((prior, previousIndex) => ({
|
|
49
|
+
incomingIndex,
|
|
50
|
+
previousIndex,
|
|
51
|
+
score: workEventSimilarity(prior, next),
|
|
52
|
+
})))
|
|
53
|
+
.filter((match) => match.score >= minimumScore)
|
|
54
|
+
.sort((left, right) => right.score - left.score ||
|
|
55
|
+
left.incomingIndex - right.incomingIndex ||
|
|
56
|
+
left.previousIndex - right.previousIndex);
|
|
57
|
+
if (previous.length <= 16 && incoming.length <= 16) {
|
|
58
|
+
const byIncoming = new Map();
|
|
59
|
+
for (const candidate of candidates) {
|
|
60
|
+
byIncoming.set(candidate.incomingIndex, [
|
|
61
|
+
...(byIncoming.get(candidate.incomingIndex) ?? []),
|
|
62
|
+
candidate,
|
|
63
|
+
]);
|
|
64
|
+
}
|
|
65
|
+
const memo = new Map();
|
|
66
|
+
const solve = (incomingIndex, claimedMask) => {
|
|
67
|
+
if (incomingIndex >= incoming.length)
|
|
68
|
+
return { score: 0, matches: [] };
|
|
69
|
+
const key = `${incomingIndex}:${claimedMask}`;
|
|
70
|
+
const cached = memo.get(key);
|
|
71
|
+
if (cached)
|
|
72
|
+
return cached;
|
|
73
|
+
let best = solve(incomingIndex + 1, claimedMask);
|
|
74
|
+
for (const candidate of byIncoming.get(incomingIndex) ?? []) {
|
|
75
|
+
const bit = 1 << candidate.previousIndex;
|
|
76
|
+
if ((claimedMask & bit) !== 0)
|
|
77
|
+
continue;
|
|
78
|
+
const rest = solve(incomingIndex + 1, claimedMask | bit);
|
|
79
|
+
const option = {
|
|
80
|
+
score: candidate.score + rest.score,
|
|
81
|
+
matches: [candidate, ...rest.matches],
|
|
82
|
+
};
|
|
83
|
+
if (option.score > best.score ||
|
|
84
|
+
(option.score === best.score &&
|
|
85
|
+
option.matches.length > best.matches.length))
|
|
86
|
+
best = option;
|
|
87
|
+
}
|
|
88
|
+
memo.set(key, best);
|
|
89
|
+
return best;
|
|
90
|
+
};
|
|
91
|
+
return solve(0, 0).matches.sort((left, right) => left.incomingIndex - right.incomingIndex);
|
|
92
|
+
}
|
|
93
|
+
const claimedIncoming = new Set();
|
|
94
|
+
const claimedPrevious = new Set();
|
|
95
|
+
const matches = [];
|
|
96
|
+
for (const candidate of candidates) {
|
|
97
|
+
if (claimedIncoming.has(candidate.incomingIndex) ||
|
|
98
|
+
claimedPrevious.has(candidate.previousIndex))
|
|
99
|
+
continue;
|
|
100
|
+
claimedIncoming.add(candidate.incomingIndex);
|
|
101
|
+
claimedPrevious.add(candidate.previousIndex);
|
|
102
|
+
matches.push(candidate);
|
|
103
|
+
}
|
|
104
|
+
return matches.sort((left, right) => left.incomingIndex - right.incomingIndex);
|
|
105
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
function validDigest(value) {
|
|
2
|
+
return /^[a-f0-9]{64}$/u.test(value);
|
|
3
|
+
}
|
|
4
|
+
export function productionEligibilityErrors(manifest) {
|
|
5
|
+
const errors = [];
|
|
6
|
+
if (manifest.schemaVersion !== 1)
|
|
7
|
+
errors.push('manifest_schema_unsupported');
|
|
8
|
+
if (!manifest.modelId)
|
|
9
|
+
errors.push('model_id_missing');
|
|
10
|
+
if (!validDigest(manifest.artifactSha256))
|
|
11
|
+
errors.push('artifact_digest_invalid');
|
|
12
|
+
if (!manifest.quantization)
|
|
13
|
+
errors.push('quantization_missing');
|
|
14
|
+
if (!manifest.sourceModel)
|
|
15
|
+
errors.push('source_model_missing');
|
|
16
|
+
if (!manifest.datasetRevision)
|
|
17
|
+
errors.push('dataset_revision_missing');
|
|
18
|
+
for (const [name, evidence] of Object.entries({
|
|
19
|
+
conversationSplit: manifest.conversationSplit,
|
|
20
|
+
factorizedCoverage: manifest.factorizedCoverage,
|
|
21
|
+
publicAcceptance: manifest.publicAcceptance,
|
|
22
|
+
privateAcceptance: manifest.privateAcceptance,
|
|
23
|
+
quantizationValidation: manifest.quantizationValidation,
|
|
24
|
+
targetHardwareBenchmark: manifest.targetHardwareBenchmark,
|
|
25
|
+
})) {
|
|
26
|
+
if (!evidence?.passed)
|
|
27
|
+
errors.push(`${name}_not_passed`);
|
|
28
|
+
if (!evidence || !validDigest(evidence.reportSha256))
|
|
29
|
+
errors.push(`${name}_report_digest_invalid`);
|
|
30
|
+
}
|
|
31
|
+
if (!manifest.modelOwnerApproved)
|
|
32
|
+
errors.push('model_owner_not_approved');
|
|
33
|
+
return errors;
|
|
34
|
+
}
|
|
35
|
+
export function assertProductionEligibleModel(manifest) {
|
|
36
|
+
const errors = productionEligibilityErrors(manifest);
|
|
37
|
+
if (errors.length > 0)
|
|
38
|
+
throw new Error(`Work-event model is not production eligible: ${errors.join(', ')}`);
|
|
39
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
import { yomiDataPath } from '../util/data-dir.js';
|
|
5
|
+
function digest(value) {
|
|
6
|
+
return createHash('sha256').update(value).digest('hex');
|
|
7
|
+
}
|
|
8
|
+
function persist(revision) {
|
|
9
|
+
return {
|
|
10
|
+
eventIdHash: digest(revision.eventId),
|
|
11
|
+
chatIdHash: digest(revision.chatId),
|
|
12
|
+
schemaVersion: 1,
|
|
13
|
+
revision: revision.revision,
|
|
14
|
+
previousRevision: revision.previousRevision,
|
|
15
|
+
active: revision.active,
|
|
16
|
+
evidence: revision.evidence.map((item) => ({
|
|
17
|
+
messageIdHash: digest(item.messageId),
|
|
18
|
+
sentAt: item.sentAt,
|
|
19
|
+
speaker: item.speaker,
|
|
20
|
+
})),
|
|
21
|
+
dialogue: revision.dialogue.map((item) => ({
|
|
22
|
+
...item,
|
|
23
|
+
messageId: digest(item.messageId),
|
|
24
|
+
})),
|
|
25
|
+
createsWork: revision.createsWork,
|
|
26
|
+
workConfidence: revision.workConfidence,
|
|
27
|
+
action: revision.action
|
|
28
|
+
? {
|
|
29
|
+
predicateHash: digest(revision.action.predicate),
|
|
30
|
+
objectHash: revision.action.object
|
|
31
|
+
? digest(revision.action.object)
|
|
32
|
+
: null,
|
|
33
|
+
expectedOutcomeHash: revision.action.expectedOutcome
|
|
34
|
+
? digest(revision.action.expectedOutcome)
|
|
35
|
+
: null,
|
|
36
|
+
dueAt: revision.action.dueAt,
|
|
37
|
+
confidence: revision.action.confidence,
|
|
38
|
+
evidenceMessageIdHashes: revision.action.evidenceMessageIds.map(digest),
|
|
39
|
+
}
|
|
40
|
+
: null,
|
|
41
|
+
ownership: {
|
|
42
|
+
state: revision.ownership.state,
|
|
43
|
+
confidence: revision.ownership.confidence,
|
|
44
|
+
evidenceMessageIdHashes: revision.ownership.evidenceMessageIds.map(digest),
|
|
45
|
+
owners: revision.ownership.owners.map((owner) => ({
|
|
46
|
+
kind: owner.kind,
|
|
47
|
+
stableIdHash: owner.stableId ? digest(owner.stableId) : null,
|
|
48
|
+
displayNameHash: owner.displayName ? digest(owner.displayName) : null,
|
|
49
|
+
})),
|
|
50
|
+
},
|
|
51
|
+
dependency: {
|
|
52
|
+
state: revision.dependency.state,
|
|
53
|
+
confidence: revision.dependency.confidence,
|
|
54
|
+
evidenceMessageIdHashes: revision.dependency.evidenceMessageIds.map(digest),
|
|
55
|
+
on: revision.dependency.on
|
|
56
|
+
? {
|
|
57
|
+
kind: revision.dependency.on.kind,
|
|
58
|
+
stableIdHash: revision.dependency.on.stableId
|
|
59
|
+
? digest(revision.dependency.on.stableId)
|
|
60
|
+
: null,
|
|
61
|
+
displayNameHash: revision.dependency.on.displayName
|
|
62
|
+
? digest(revision.dependency.on.displayName)
|
|
63
|
+
: null,
|
|
64
|
+
}
|
|
65
|
+
: null,
|
|
66
|
+
},
|
|
67
|
+
lifecycle: revision.lifecycle,
|
|
68
|
+
completion: {
|
|
69
|
+
...revision.completion,
|
|
70
|
+
evidenceMessageIds: revision.completion.evidenceMessageIds.map(digest),
|
|
71
|
+
},
|
|
72
|
+
sourceModel: revision.sourceModel,
|
|
73
|
+
inferredAt: revision.inferredAt,
|
|
74
|
+
reasons: revision.reasons,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/** Metadata-only audit history; decrypted text remains in the secure UI cache. */
|
|
78
|
+
export class WorkEventRevisionStore {
|
|
79
|
+
path;
|
|
80
|
+
maxRevisions;
|
|
81
|
+
writeQueue = Promise.resolve();
|
|
82
|
+
constructor(path = yomiDataPath('work-event-revisions.json'), maxRevisions = 2000) {
|
|
83
|
+
this.path = path;
|
|
84
|
+
this.maxRevisions = maxRevisions;
|
|
85
|
+
}
|
|
86
|
+
async append(revision) {
|
|
87
|
+
await this.appendMany([revision]);
|
|
88
|
+
}
|
|
89
|
+
async appendMany(incoming) {
|
|
90
|
+
if (incoming.length === 0)
|
|
91
|
+
return;
|
|
92
|
+
const run = async () => {
|
|
93
|
+
const current = await this.read();
|
|
94
|
+
const rows = incoming.map(persist);
|
|
95
|
+
const incomingKeys = new Set(rows.map((row) => `${row.eventIdHash}:${row.revision}`));
|
|
96
|
+
const revisions = [
|
|
97
|
+
...rows,
|
|
98
|
+
...current.filter((item) => !incomingKeys.has(`${item.eventIdHash}:${item.revision}`)),
|
|
99
|
+
]
|
|
100
|
+
.sort((left, right) => right.inferredAt - left.inferredAt)
|
|
101
|
+
.slice(0, this.maxRevisions);
|
|
102
|
+
await mkdir(dirname(this.path), { recursive: true });
|
|
103
|
+
const temporary = `${this.path}.tmp-${process.pid}`;
|
|
104
|
+
await writeFile(temporary, JSON.stringify({ schemaVersion: 1, revisions }), { mode: 0o600 });
|
|
105
|
+
await rename(temporary, this.path);
|
|
106
|
+
};
|
|
107
|
+
this.writeQueue = this.writeQueue.then(run, run);
|
|
108
|
+
await this.writeQueue;
|
|
109
|
+
}
|
|
110
|
+
async list() {
|
|
111
|
+
await this.writeQueue;
|
|
112
|
+
return this.read();
|
|
113
|
+
}
|
|
114
|
+
async read() {
|
|
115
|
+
try {
|
|
116
|
+
const value = JSON.parse(await readFile(this.path, 'utf8'));
|
|
117
|
+
return value.schemaVersion === 1 && Array.isArray(value.revisions)
|
|
118
|
+
? value.revisions
|
|
119
|
+
: [];
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
function sameValue(left, right) {
|
|
2
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
3
|
+
}
|
|
4
|
+
function evidenceUnion(previous, incoming) {
|
|
5
|
+
return [
|
|
6
|
+
...new Map([...previous, ...incoming].map((item) => [item.messageId, item])).values(),
|
|
7
|
+
].sort((left, right) => left.sentAt - right.sentAt);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Append a new authoritative revision without making lifecycle monotonic.
|
|
11
|
+
* A later event window may disprove an earlier completion and reopen work.
|
|
12
|
+
*/
|
|
13
|
+
export function reviseWorkEvent(previous, inference, explicitReasons = []) {
|
|
14
|
+
if (!previous) {
|
|
15
|
+
return {
|
|
16
|
+
...inference,
|
|
17
|
+
schemaVersion: 1,
|
|
18
|
+
revision: 1,
|
|
19
|
+
previousRevision: null,
|
|
20
|
+
active: inference.createsWork,
|
|
21
|
+
reasons: ['created', ...explicitReasons],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
if (previous.eventId !== inference.eventId) {
|
|
25
|
+
throw new Error('Cannot revise a different work event');
|
|
26
|
+
}
|
|
27
|
+
const evidence = evidenceUnion(previous.evidence, inference.evidence);
|
|
28
|
+
const evidenceAdded = evidence.length > previous.evidence.length;
|
|
29
|
+
const inferenceChanged = !sameValue({
|
|
30
|
+
dialogue: previous.dialogue,
|
|
31
|
+
createsWork: previous.createsWork,
|
|
32
|
+
action: previous.action,
|
|
33
|
+
ownership: previous.ownership,
|
|
34
|
+
dependency: previous.dependency,
|
|
35
|
+
lifecycle: previous.lifecycle,
|
|
36
|
+
completion: previous.completion,
|
|
37
|
+
}, {
|
|
38
|
+
dialogue: inference.dialogue,
|
|
39
|
+
createsWork: inference.createsWork,
|
|
40
|
+
action: inference.action,
|
|
41
|
+
ownership: inference.ownership,
|
|
42
|
+
dependency: inference.dependency,
|
|
43
|
+
lifecycle: inference.lifecycle,
|
|
44
|
+
completion: inference.completion,
|
|
45
|
+
});
|
|
46
|
+
const wasClosed = ['accepted', 'cancelled', 'resolved'].includes(previous.lifecycle);
|
|
47
|
+
const isOpen = ['proposed', 'committed', 'in_progress', 'blocked'].includes(inference.lifecycle);
|
|
48
|
+
const reasons = [
|
|
49
|
+
...(evidenceAdded ? ['evidence_added'] : []),
|
|
50
|
+
...(inferenceChanged ? ['inference_changed'] : []),
|
|
51
|
+
...(wasClosed && isOpen ? ['reopened'] : []),
|
|
52
|
+
...explicitReasons,
|
|
53
|
+
];
|
|
54
|
+
return {
|
|
55
|
+
...inference,
|
|
56
|
+
evidence,
|
|
57
|
+
schemaVersion: 1,
|
|
58
|
+
revision: previous.revision + 1,
|
|
59
|
+
previousRevision: previous.revision,
|
|
60
|
+
active: inference.createsWork &&
|
|
61
|
+
!['accepted', 'cancelled', 'resolved'].includes(inference.lifecycle),
|
|
62
|
+
reasons: [...new Set(reasons)],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export function excludeWorkEvent(previous, temporary = false) {
|
|
66
|
+
return {
|
|
67
|
+
...previous,
|
|
68
|
+
revision: previous.revision + 1,
|
|
69
|
+
previousRevision: previous.revision,
|
|
70
|
+
active: false,
|
|
71
|
+
reasons: [temporary ? 'temporarily_excluded' : 'excluded'],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export function splitWorkEvent(previous, childInferences) {
|
|
75
|
+
if (childInferences.length < 2)
|
|
76
|
+
throw new Error('A split requires at least two child events');
|
|
77
|
+
const previousEvidence = new Set(previous.evidence.map((item) => item.messageId));
|
|
78
|
+
const childIds = new Set();
|
|
79
|
+
for (const child of childInferences) {
|
|
80
|
+
if (childIds.has(child.eventId))
|
|
81
|
+
throw new Error('Split children require distinct event ids');
|
|
82
|
+
childIds.add(child.eventId);
|
|
83
|
+
if (child.evidence.length === 0)
|
|
84
|
+
throw new Error('Split child requires evidence');
|
|
85
|
+
for (const evidence of child.evidence) {
|
|
86
|
+
if (!previousEvidence.has(evidence.messageId))
|
|
87
|
+
throw new Error('Split child contains evidence outside the parent event');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const superseded = {
|
|
91
|
+
...previous,
|
|
92
|
+
revision: previous.revision + 1,
|
|
93
|
+
previousRevision: previous.revision,
|
|
94
|
+
active: false,
|
|
95
|
+
reasons: ['split'],
|
|
96
|
+
};
|
|
97
|
+
return {
|
|
98
|
+
superseded,
|
|
99
|
+
children: childInferences.map((child) => ({
|
|
100
|
+
...reviseWorkEvent(null, child, ['split']),
|
|
101
|
+
previousRevision: previous.revision,
|
|
102
|
+
})),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
export function mergeWorkEvents(previous, mergedInference) {
|
|
106
|
+
if (previous.length < 2)
|
|
107
|
+
throw new Error('A merge requires at least two source events');
|
|
108
|
+
const sourceEvidence = new Set(previous.flatMap((event) => event.evidence.map((item) => item.messageId)));
|
|
109
|
+
for (const evidence of mergedInference.evidence) {
|
|
110
|
+
if (!sourceEvidence.has(evidence.messageId))
|
|
111
|
+
throw new Error('Merged event contains unknown evidence');
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
superseded: previous.map((event) => ({
|
|
115
|
+
...event,
|
|
116
|
+
revision: event.revision + 1,
|
|
117
|
+
previousRevision: event.revision,
|
|
118
|
+
active: false,
|
|
119
|
+
reasons: ['merged'],
|
|
120
|
+
})),
|
|
121
|
+
merged: reviseWorkEvent(null, mergedInference, ['merged']),
|
|
122
|
+
};
|
|
123
|
+
}
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"engines": {
|
|
4
4
|
"node": ">=22.13.0 <23.0.0 || >=23.4.0"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.6.0",
|
|
7
7
|
"description": "Open-source personal LINE MCP server for AI agents. Read, reply, send images, and search LINE conversations locally with Letter-Sealing E2EE.",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"license": "MIT",
|
|
@@ -45,11 +45,32 @@
|
|
|
45
45
|
"lint:fix": "biome check --write src/",
|
|
46
46
|
"lint:comments": "node scripts/lint-comments.mjs",
|
|
47
47
|
"hooks:install": "git config core.hooksPath .githooks",
|
|
48
|
-
"pretest": "npm run lint:comments",
|
|
48
|
+
"pretest": "npm run lint:comments && npm run test:fixture-hygiene",
|
|
49
49
|
"format": "biome format --write src/",
|
|
50
50
|
"test": "bun test",
|
|
51
|
+
"predesktop:build": "npm run build",
|
|
52
|
+
"desktop:build": "bash scripts/build-swift-desktop.sh",
|
|
53
|
+
"desktop:clean": "node scripts/clean-desktop-artifacts.mjs",
|
|
54
|
+
"desktop:clean:all": "node scripts/clean-desktop-artifacts.mjs --all",
|
|
55
|
+
"desktop:harness": "node scripts/desktop-harness.mjs",
|
|
56
|
+
"desktop:qos": "node scripts/desktop-qos-harness.mjs",
|
|
57
|
+
"desktop:flex-parity": "npm run build && node scripts/flex-layout-parity-harness.mjs",
|
|
58
|
+
"desktop:swift-quality": "bash scripts/swift-quality.sh",
|
|
59
|
+
"desktop:i18n-harness": "node scripts/swift-i18n-harness.mjs",
|
|
60
|
+
"desktop:restart": "node scripts/desktop-harness.mjs --restart",
|
|
61
|
+
"desktop:run": "./desktop/mac-native/Yomi.app/Contents/MacOS/Yomi",
|
|
62
|
+
"desktop:core:macos": "node scripts/package-desktop-core.mjs --platform macos --output build/desktop-core-macos --semantic full",
|
|
63
|
+
"desktop:core:windows": "node scripts/package-desktop-core.mjs --platform windows --output build/desktop-core-windows --semantic full",
|
|
64
|
+
"desktop:package:harness": "node scripts/desktop-package-harness.mjs",
|
|
65
|
+
"desktop:windows:build": "cargo build --manifest-path desktop/Cargo.toml --release --target x86_64-pc-windows-msvc",
|
|
66
|
+
"desktop:windows:lint": "cargo fmt --manifest-path desktop/Cargo.toml -- --check && cargo clippy --manifest-path desktop/Cargo.toml --target x86_64-pc-windows-msvc -- -D warnings",
|
|
67
|
+
"desktop:windows:qc": "npm run typecheck && npm run lint && npm run lint:comments && cargo test --manifest-path desktop/Cargo.toml && npm run desktop:windows:lint",
|
|
68
|
+
"desktop:windows:i18n": "node scripts/windows-i18n-harness.mjs",
|
|
69
|
+
"desktop:windows:harness": "node scripts/windows-desktop-harness.mjs",
|
|
51
70
|
"version": "node scripts/sync-version.mjs && git add src/version.ts",
|
|
52
|
-
"prepublishOnly": "npm run build"
|
|
71
|
+
"prepublishOnly": "npm run build",
|
|
72
|
+
"test:fixture-hygiene": "node scripts/test-fixture-hygiene.mjs",
|
|
73
|
+
"desktop:install-local": "bash scripts/install-swift-desktop-local.sh"
|
|
53
74
|
},
|
|
54
75
|
"dependencies": {
|
|
55
76
|
"@huggingface/transformers": "^4.2.0",
|
package/run.mjs
CHANGED
|
@@ -46,6 +46,15 @@ const ext = built ? '.js' : '.ts';
|
|
|
46
46
|
/** Import a module by its path under src/, resolved to dist/ when built. */
|
|
47
47
|
const load = (path) => import(new URL(path + ext, base).href);
|
|
48
48
|
|
|
49
|
+
const writeStdout = (text) =>
|
|
50
|
+
new Promise((resolve, reject) => {
|
|
51
|
+
process.stdout.write(text, (error) => {
|
|
52
|
+
if (error) reject(error);
|
|
53
|
+
else resolve();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
const writeJson = (value) => writeStdout(`${JSON.stringify(value)}\n`);
|
|
57
|
+
|
|
49
58
|
const args = process.argv.slice(2);
|
|
50
59
|
const cmd = args[0] ?? 'serve';
|
|
51
60
|
|
|
@@ -80,10 +89,91 @@ switch (cmd) {
|
|
|
80
89
|
const { cliListConversationsJson } = await load('cli/query');
|
|
81
90
|
const limit = args[1] && args[1] !== 'all' ? parseInt(args[1], 10) : undefined;
|
|
82
91
|
const attentionOnly = args.includes('--attention-only');
|
|
83
|
-
const firstPage = args.includes('--first-page');
|
|
84
92
|
const semantic = args.includes('--semantic');
|
|
85
|
-
const json = await cliListConversationsJson({ limit, attentionOnly,
|
|
86
|
-
|
|
93
|
+
const json = await cliListConversationsJson({ limit, attentionOnly, semantic });
|
|
94
|
+
await writeJson(json);
|
|
95
|
+
process.exit(0);
|
|
96
|
+
}
|
|
97
|
+
case 'desktop-inbox': {
|
|
98
|
+
// Keep one authenticated LINE runtime for the complete desktop startup
|
|
99
|
+
// pass. Its resume result is the session authority; do not infer logout
|
|
100
|
+
// from an empty list, a cache miss, or a second probe process.
|
|
101
|
+
const { LineProtocolService } = await load('line/core/service');
|
|
102
|
+
const { cliListConversationsJson } = await load('cli/query');
|
|
103
|
+
const service = new LineProtocolService();
|
|
104
|
+
const connected = await service.resumeSession();
|
|
105
|
+
if (!connected) {
|
|
106
|
+
await writeJson({
|
|
107
|
+
session: { connected: false, reason: service.loginReason ?? 'unavailable' },
|
|
108
|
+
chats: [],
|
|
109
|
+
});
|
|
110
|
+
process.exit(0);
|
|
111
|
+
}
|
|
112
|
+
const chats = await cliListConversationsJson({}, service);
|
|
113
|
+
await writeJson({ session: { connected: true, reason: null }, chats });
|
|
114
|
+
process.exit(0);
|
|
115
|
+
}
|
|
116
|
+
case 'desktop-startup-messages': {
|
|
117
|
+
// One shared runtime warms all requested rooms at launch. This is a
|
|
118
|
+
// startup batch, not a click-triggered series of independent sessions.
|
|
119
|
+
const { LineProtocolService } = await load('line/core/service');
|
|
120
|
+
const { cliGetChatMessagesJson } = await load('cli/query');
|
|
121
|
+
const chatIds = args.slice(1).filter(Boolean);
|
|
122
|
+
const service = new LineProtocolService();
|
|
123
|
+
const connected = await service.resumeSession();
|
|
124
|
+
if (!connected) {
|
|
125
|
+
await writeJson({
|
|
126
|
+
session: { connected: false, reason: service.loginReason ?? 'unavailable' },
|
|
127
|
+
messages: {},
|
|
128
|
+
});
|
|
129
|
+
process.exit(0);
|
|
130
|
+
}
|
|
131
|
+
const messages = {};
|
|
132
|
+
for (const chatId of chatIds) {
|
|
133
|
+
messages[chatId] = await cliGetChatMessagesJson(chatId, 40, service);
|
|
134
|
+
}
|
|
135
|
+
await writeJson({ session: { connected: true, reason: null }, messages });
|
|
136
|
+
process.exit(0);
|
|
137
|
+
}
|
|
138
|
+
case 'work-item': {
|
|
139
|
+
const operation = args[1] || 'create';
|
|
140
|
+
let input = '';
|
|
141
|
+
for await (const chunk of process.stdin) input += chunk;
|
|
142
|
+
let payload;
|
|
143
|
+
try {
|
|
144
|
+
payload = JSON.parse(input || '{}');
|
|
145
|
+
} catch {
|
|
146
|
+
console.error(JSON.stringify({ error: 'invalid JSON payload' }));
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
const { cliCreateWorkItemDraftJson, cliWorkItemOperationJson } = await load('cli/work-item');
|
|
150
|
+
try {
|
|
151
|
+
const result = operation === 'create'
|
|
152
|
+
? await cliCreateWorkItemDraftJson(
|
|
153
|
+
payload.messages || payload.message,
|
|
154
|
+
payload.options || {},
|
|
155
|
+
)
|
|
156
|
+
: await cliWorkItemOperationJson(operation, payload.draft);
|
|
157
|
+
await writeJson(result);
|
|
158
|
+
process.exit(0);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.error(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
case 'ambient-observe': {
|
|
165
|
+
const { scanAmbientObservations } = await load('ambient/ambient-observer');
|
|
166
|
+
const since = args[1] ? Number(args[1]) : undefined;
|
|
167
|
+
const report = await scanAmbientObservations(Number.isFinite(since) ? since : undefined);
|
|
168
|
+
// Work-event promotion intentionally abstains until the factorized model
|
|
169
|
+
// contract passes its independent acceptance suite. Ambient candidates
|
|
170
|
+
// remain available as revisable evidence; no flat classifier is allowed
|
|
171
|
+
// to create authoritative work items.
|
|
172
|
+
await writeJson({ ...report, workItems: [] });
|
|
173
|
+
process.exit(0);
|
|
174
|
+
}
|
|
175
|
+
case 'work-items': {
|
|
176
|
+
await writeJson([]);
|
|
87
177
|
process.exit(0);
|
|
88
178
|
}
|
|
89
179
|
case 'messages':
|
|
@@ -96,7 +186,20 @@ switch (cmd) {
|
|
|
96
186
|
process.exit(1);
|
|
97
187
|
}
|
|
98
188
|
const json = await cliGetChatMessagesJson(chatId, count);
|
|
99
|
-
|
|
189
|
+
await writeJson(json);
|
|
190
|
+
process.exit(0);
|
|
191
|
+
}
|
|
192
|
+
case 'rich-menu': {
|
|
193
|
+
const chatId = args[1];
|
|
194
|
+
if (!chatId) {
|
|
195
|
+
console.error(JSON.stringify({ error: 'chatId required' }));
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
const { LineProtocolService } = await load('line/core/service');
|
|
199
|
+
const service = new LineProtocolService();
|
|
200
|
+
if (!(await service.resumeSession())) process.exit(1);
|
|
201
|
+
const menu = await service.client.getBuddyRichMenu(chatId);
|
|
202
|
+
await writeJson(menu);
|
|
100
203
|
process.exit(0);
|
|
101
204
|
}
|
|
102
205
|
case 'attention-classify': {
|
|
@@ -104,13 +207,13 @@ switch (cmd) {
|
|
|
104
207
|
let input = '';
|
|
105
208
|
for await (const chunk of process.stdin) input += chunk;
|
|
106
209
|
const items = JSON.parse(input || '[]');
|
|
107
|
-
|
|
210
|
+
await writeJson(await cliClassifyAttentionJson(items));
|
|
108
211
|
process.exit(0);
|
|
109
212
|
}
|
|
110
213
|
case 'list-stickers': {
|
|
111
214
|
const { cliListStickersJson } = await load('cli/query');
|
|
112
215
|
const language = args[1] || 'zh_TW';
|
|
113
|
-
|
|
216
|
+
await writeJson(await cliListStickersJson(language));
|
|
114
217
|
process.exit(0);
|
|
115
218
|
}
|
|
116
219
|
case 'sticker-previews': {
|
|
@@ -122,7 +225,7 @@ switch (cmd) {
|
|
|
122
225
|
console.error(JSON.stringify({ error: 'packageId required' }));
|
|
123
226
|
process.exit(1);
|
|
124
227
|
}
|
|
125
|
-
|
|
228
|
+
await writeJson(await cliStickerPreviewsJson(packageId, limit, language));
|
|
126
229
|
process.exit(0);
|
|
127
230
|
}
|
|
128
231
|
case 'send-sticker': {
|
|
@@ -215,6 +318,32 @@ switch (cmd) {
|
|
|
215
318
|
}));
|
|
216
319
|
process.exit(0);
|
|
217
320
|
}
|
|
321
|
+
case 'send-audio': {
|
|
322
|
+
const chatId = args[1];
|
|
323
|
+
const filePath = args[2];
|
|
324
|
+
const fileName = args[3];
|
|
325
|
+
if (!chatId || !filePath) {
|
|
326
|
+
console.error(JSON.stringify({ error: 'chatId and filePath required' }));
|
|
327
|
+
process.exit(1);
|
|
328
|
+
}
|
|
329
|
+
const { LineProtocolService } = await load('line/core/service');
|
|
330
|
+
const { readFile } = await import('node:fs/promises');
|
|
331
|
+
const { basename } = await import('node:path');
|
|
332
|
+
const service = new LineProtocolService();
|
|
333
|
+
if (!(await service.resumeSession())) process.exit(1);
|
|
334
|
+
const bytes = await readFile(filePath);
|
|
335
|
+
if (bytes.length === 0) {
|
|
336
|
+
console.error(JSON.stringify({ error: 'audio is empty' }));
|
|
337
|
+
process.exit(1);
|
|
338
|
+
}
|
|
339
|
+
const sent = await service.sendAudio(chatId, bytes, fileName || basename(filePath));
|
|
340
|
+
console.log(JSON.stringify({
|
|
341
|
+
sent: true,
|
|
342
|
+
messageId: sent?.messageId ?? sent?.id ?? null,
|
|
343
|
+
oid: sent?.oid ?? null,
|
|
344
|
+
}));
|
|
345
|
+
process.exit(0);
|
|
346
|
+
}
|
|
218
347
|
case 'media-preview': {
|
|
219
348
|
const chatId = args[1];
|
|
220
349
|
const messageId = args[2];
|
|
@@ -227,11 +356,21 @@ switch (cmd) {
|
|
|
227
356
|
const service = new LineProtocolService();
|
|
228
357
|
if (!(await service.resumeSession())) process.exit(1);
|
|
229
358
|
const result = await fetchLineMessageMedia(service, chatId, messageId, args[3] !== 'original');
|
|
230
|
-
|
|
359
|
+
if (args.includes('--file-output')) {
|
|
360
|
+
const { mkdtemp, writeFile } = await import('node:fs/promises');
|
|
361
|
+
const { join } = await import('node:path');
|
|
362
|
+
const { tmpdir } = await import('node:os');
|
|
363
|
+
const directory = await mkdtemp(join(tmpdir(), 'yomi-media-'));
|
|
364
|
+
const filePath = join(directory, 'payload');
|
|
365
|
+
await writeFile(filePath, result.bytes, { mode: 0o600 });
|
|
366
|
+
await writeJson({ filePath, mimeType: result.mimeType, fileName: result.fileName ?? null });
|
|
367
|
+
process.exit(0);
|
|
368
|
+
}
|
|
369
|
+
await writeJson({
|
|
231
370
|
data: result.bytes.toString('base64'),
|
|
232
371
|
mimeType: result.mimeType,
|
|
233
372
|
fileName: result.fileName ?? null,
|
|
234
|
-
})
|
|
373
|
+
});
|
|
235
374
|
process.exit(0);
|
|
236
375
|
}
|
|
237
376
|
case 'mark-read': {
|
|
@@ -246,6 +385,20 @@ switch (cmd) {
|
|
|
246
385
|
console.log(JSON.stringify(res));
|
|
247
386
|
process.exit(0);
|
|
248
387
|
}
|
|
388
|
+
case 'set-chat-muted': {
|
|
389
|
+
const chatId = args[1];
|
|
390
|
+
const disabled = args[2] === 'true';
|
|
391
|
+
if (!chatId?.startsWith('u') || !['true', 'false'].includes(args[2])) {
|
|
392
|
+
console.error(JSON.stringify({ error: '1:1 chatId and true|false required' }));
|
|
393
|
+
process.exit(1);
|
|
394
|
+
}
|
|
395
|
+
const { LineProtocolService } = await load('line/core/service');
|
|
396
|
+
const service = new LineProtocolService();
|
|
397
|
+
if (!(await service.resumeSession())) process.exit(1);
|
|
398
|
+
await service.client.updateContactNotificationDisabled(chatId, disabled);
|
|
399
|
+
console.log(JSON.stringify({ success: true, chatId, isMuted: disabled }));
|
|
400
|
+
process.exit(0);
|
|
401
|
+
}
|
|
249
402
|
case 'summary':
|
|
250
403
|
case 'recent':
|
|
251
404
|
case 'query': {
|