fuli-context 0.3.0 → 0.5.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.en.md +336 -0
- package/README.md +220 -174
- package/dist/web/assets/ConnectionsPage-CCKWm-6p.js +1 -0
- package/dist/web/assets/KnowledgeInspector-BZkB2wJn.js +1 -0
- package/dist/web/assets/KnowledgeInspector-CrGYxEob.css +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-CZAF_OlN.css +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-DlvcLwlL.js +1 -0
- package/dist/web/assets/KnowledgePage-CNQXd15y.js +1 -0
- package/dist/web/assets/KnowledgeWorkspace-CXPE4GCD.js +4 -0
- package/dist/web/assets/OverviewPage-ul7g7HXh.js +1 -0
- package/dist/web/assets/PersonalProfilePage-CC40Pw2L.js +1 -0
- package/dist/web/assets/PersonalProfilePage-S8ZcG2LU.css +1 -0
- package/dist/web/assets/PersonalProjectsPage-CPIEcShD.js +1 -0
- package/dist/web/assets/PublicProjectsPage-C700sR7z.js +1 -0
- package/dist/web/assets/ReviewPage-7MDDUDgA.js +1 -0
- package/dist/web/assets/console-B_Ena2DS.js +5 -0
- package/dist/web/assets/identity-CjBwYSMX.js +1 -0
- package/dist/web/assets/{index-Devf7glZ.css → index-CpLDx3of.css} +1 -1
- package/dist/web/assets/index-DAzeFJSc.js +2 -0
- package/dist/web/assets/model-C-5RVIJ2.js +1 -0
- package/dist/web/assets/{paths-B790iniO.js → paths-BkULqmGI.js} +1 -1
- package/dist/web/assets/{vue-router-BYTAoWT3.js → vue-router-DnD3hUm7.js} +1 -1
- package/dist/web/index.html +5 -6
- package/graph-provider/fuli_graph/app.py +48 -0
- package/graph-provider/fuli_graph/common_knowledge.py +443 -0
- package/graph-provider/fuli_graph/common_knowledge_models.py +54 -0
- package/graph-provider/fuli_graph/graph_models.py +9 -0
- package/graph-provider/fuli_graph/graph_query.py +25 -0
- package/graph-provider/fuli_graph/knowledge_audit.py +211 -0
- package/graph-provider/fuli_graph/knowledge_feedback_models.py +69 -0
- package/graph-provider/fuli_graph/knowledge_management.py +37 -0
- package/graph-provider/fuli_graph/knowledge_search.py +24 -2
- package/graph-provider/fuli_graph/models.py +69 -0
- package/graph-provider/fuli_graph/search_projection.py +59 -0
- package/graph-provider/fuli_graph/store_knowledge.py +2 -0
- package/graph-provider/pyproject.toml +1 -1
- package/npm-shrinkwrap.json +97 -2
- package/package.json +3 -1
- package/skills/capturing-session-knowledge/SKILL.md +83 -13
- package/src/agent-tools/graph-definitions.js +181 -13
- package/src/agent-tools/graph-handlers.js +19 -0
- package/src/cli/command-registry.js +3 -58
- package/src/cli/local-runtime-command.js +16 -2
- package/src/cli/local-runtime-options.js +6 -2
- package/src/cli.js +2 -37
- package/src/graphiti/agent-knowledge-workflows.js +543 -0
- package/src/graphiti/federated-application.js +85 -104
- package/src/graphiti/knowledge-provider-mapping.js +91 -0
- package/src/graphiti/provider-client.js +15 -0
- package/src/graphiti/source-uri.js +29 -0
- package/src/http/request-policy.js +37 -2
- package/src/local-runtime/lifecycle.js +11 -1
- package/src/mcp/common-knowledge-preview-tokens.js +67 -0
- package/src/mcp/create-mcp-server.js +43 -7
- package/src/mcp/instructions.js +6 -7
- package/src/mcp/lifecycle-audit.js +27 -0
- package/src/mcp/task-context-registry.js +93 -0
- package/src/mcp/tool-annotations.js +16 -1
- package/src/mcp/tool-result.js +24 -0
- package/src/runtime-options.js +7 -8
- package/src/server/lan-access.js +52 -0
- package/src/server.js +67 -8
- package/src/setup/agent-installation-status.js +14 -2
- package/src/setup/agents.js +12 -1
- package/src/setup/claude-code-config.js +194 -0
- package/src/setup/config-backup.js +20 -5
- package/src/setup/graph-runtime.js +78 -9
- package/dist/web/assets/ConnectionsPage-CY6H_mJJ.js +0 -1
- package/dist/web/assets/KnowledgeInspector-BSpz5Sfm.js +0 -1
- package/dist/web/assets/KnowledgeInspector-CHbB_0rC.css +0 -1
- package/dist/web/assets/KnowledgeOrganizerPage-BlEvhPrv.css +0 -1
- package/dist/web/assets/KnowledgeOrganizerPage-CvjGPlUc.js +0 -1
- package/dist/web/assets/KnowledgePage-Khuo3cNs.js +0 -1
- package/dist/web/assets/KnowledgeWorkspace-DVZusvem.js +0 -4
- package/dist/web/assets/OverviewPage-DC4C4Fhn.js +0 -1
- package/dist/web/assets/PersonalProfilePage-CSV9Mtgz.js +0 -1
- package/dist/web/assets/PersonalProfilePage-CVJ3TcRi.css +0 -1
- package/dist/web/assets/PersonalProjectsPage-A50EaseQ.js +0 -1
- package/dist/web/assets/PublicProjectsPage-BAdO_bae.js +0 -1
- package/dist/web/assets/ReviewPage-Dztk2jDG.js +0 -1
- package/dist/web/assets/console-tbvTUi6p.js +0 -1
- package/dist/web/assets/identity-B3s5CXhv.js +0 -1
- package/dist/web/assets/index-DwUDhQKc.js +0 -6
- package/dist/web/assets/model-DVT8sjy9.js +0 -1
- package/dist/web/assets/runtime-dom.esm-bundler-DttsK21F.js +0 -1
- package/src/cli/commands/candidates.js +0 -17
- package/src/cli/commands/ingestion.js +0 -32
- package/src/cli/commands/reads.js +0 -50
- package/src/cli/commands/spaces.js +0 -20
- package/src/cli/invocation.js +0 -47
- package/src/cli/migrate-command.js +0 -71
- package/src/cli/migration-destination.js +0 -117
- package/src/storage/import-json.js +0 -70
- package/src/storage/legacy-record-validation.js +0 -152
- package/src/storage/legacy-snapshot-validation.js +0 -32
package/src/mcp/instructions.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
export const MCP_INSTRUCTIONS = [
|
|
2
|
-
"Each user task: call exactly get_collaboration_preferences before other tools/answer
|
|
3
|
-
'
|
|
4
|
-
'
|
|
5
|
-
'
|
|
6
|
-
'
|
|
7
|
-
'
|
|
8
|
-
'When personal knowledge affects the final answer/action, call record_knowledge_usage. Retrieval/injection never count; it creates at most agent_confirmed, never public eligibility.',
|
|
2
|
+
"Each user task: if begin_task_context supplied hook-provided task context, apply. Otherwise call exactly get_collaboration_preferences with projectPath=cwd before other tools/answer as the fallback; never substitute a project action or Fuli tool. Apply all effective_preferences: personal-global everywhere plus only Fuli's matched project. Resolve relevant deferred_conflict first. Never guess personalProjectId. Writes enforce preferences in the actual payload; final text is not compliance.",
|
|
3
|
+
'For current-project facts use search_current_project_knowledge: exact active child first, then inheritable parent knowledge via outgoing PART_OF/USES_KNOWLEDGE_FROM within two hops. Child stable keys override. Never inherit project preferences or traverse RELATED_TO.',
|
|
4
|
+
'Before claiming facts unknown, use search_knowledge_graph. Use exact IDs for extra projects; mark pending. Widen a location miss to all_local_confirmed only after consent, then use rg only in the safe current repo/workspace. Never scan outside it. Live URLs need deployment evidence; changing state needs monitoring/Git MCP.',
|
|
5
|
+
'Use record_decision_trace for confirmed choices/rationale/validation. Use record_knowledge_feedback when retained knowledge is rejected, contradicted, outdated, or fails validation; preserve history.',
|
|
6
|
+
'Batch durable confirmed knowledge via capture_session_knowledge in exact scope. Never store secrets, transcripts, or guesses. Only human/source confirmation permits public review. Finish a hook-managed task with checkpoint_task_knowledge once: capture_candidates or retain_nothing; fallback Agents likewise flush or retain nothing.',
|
|
7
|
+
'When personal knowledge materially affects the answer/action, call record_knowledge_usage. Retrieval/injection never counts; it creates at most agent_confirmed and is never public-eligible.',
|
|
9
8
|
'Terminal-safe Markdown. Final MUST begin with sourceMarker.leadMarkdown; append sourceMarker.markdown unchanged only on match. On miss use noMatchSourceMarker leadMarkdown only (empty markdown). Never wrap markers in HTML or cite writes.'
|
|
10
9
|
].join(' ');
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { appendFileSync } from 'node:fs';
|
|
2
|
+
|
|
3
|
+
const AUDITED_TOOLS = new Set([
|
|
4
|
+
'begin_task_context',
|
|
5
|
+
'checkpoint_task_knowledge',
|
|
6
|
+
'verify_task_checkpoint'
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
export function auditLifecycleTool(
|
|
10
|
+
toolName,
|
|
11
|
+
{
|
|
12
|
+
auditPath = process.env.FULI_ACCEPTANCE_LIFECYCLE_AUDIT_PATH,
|
|
13
|
+
append = appendFileSync
|
|
14
|
+
} = {}
|
|
15
|
+
) {
|
|
16
|
+
if (!auditPath || !AUDITED_TOOLS.has(toolName)) return false;
|
|
17
|
+
try {
|
|
18
|
+
append(
|
|
19
|
+
auditPath,
|
|
20
|
+
`${JSON.stringify({ event: toolName })}\n`,
|
|
21
|
+
{ encoding: 'utf8', mode: 0o600 }
|
|
22
|
+
);
|
|
23
|
+
return true;
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
const MAX_ACTIVE_SESSIONS = 256;
|
|
4
|
+
|
|
5
|
+
export class TaskContextRegistry {
|
|
6
|
+
constructor({ tokenFactory = () => `fuli-task-${randomUUID()}` } = {}) {
|
|
7
|
+
this.tokenFactory = tokenFactory;
|
|
8
|
+
this.sessions = new Map();
|
|
9
|
+
this.tokens = new Map();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
begin({ sessionId, personalProjectId }) {
|
|
13
|
+
assertNonEmpty(sessionId, 'sessionId');
|
|
14
|
+
const previous = this.sessions.get(sessionId) ?? null;
|
|
15
|
+
if (previous) this.tokens.delete(previous.token);
|
|
16
|
+
|
|
17
|
+
const task = {
|
|
18
|
+
token: this.tokenFactory(),
|
|
19
|
+
sessionId,
|
|
20
|
+
personalProjectId: personalProjectId ?? null,
|
|
21
|
+
checkpoint: null
|
|
22
|
+
};
|
|
23
|
+
this.sessions.delete(sessionId);
|
|
24
|
+
this.sessions.set(sessionId, task);
|
|
25
|
+
this.tokens.set(task.token, task);
|
|
26
|
+
this.#evictOldest();
|
|
27
|
+
return {
|
|
28
|
+
...task,
|
|
29
|
+
previousCheckpointMissing: Boolean(previous && !previous.checkpoint)
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
checkpoint(token, checkpoint) {
|
|
34
|
+
assertNonEmpty(token, 'taskContextToken');
|
|
35
|
+
const task = this.tokens.get(token);
|
|
36
|
+
if (!task) throw new TypeError('Unknown or expired Fuli task context token');
|
|
37
|
+
if (task.checkpoint) {
|
|
38
|
+
if (sameCheckpoint(task.checkpoint, checkpoint)) return task;
|
|
39
|
+
throw new TypeError('Fuli task context has already been checkpointed');
|
|
40
|
+
}
|
|
41
|
+
task.checkpoint = Object.freeze({ ...checkpoint });
|
|
42
|
+
return task;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
context(token) {
|
|
46
|
+
assertNonEmpty(token, 'taskContextToken');
|
|
47
|
+
const task = this.tokens.get(token);
|
|
48
|
+
if (!task) throw new TypeError('Unknown or expired Fuli task context token');
|
|
49
|
+
return task;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
verify(sessionId) {
|
|
53
|
+
assertNonEmpty(sessionId, 'sessionId');
|
|
54
|
+
const task = this.sessions.get(sessionId);
|
|
55
|
+
if (!task) {
|
|
56
|
+
return {
|
|
57
|
+
status: 'not_started',
|
|
58
|
+
guidance: 'No Fuli task context was started; do not block the Agent.'
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (task.checkpoint) {
|
|
62
|
+
return {
|
|
63
|
+
status: 'checkpointed',
|
|
64
|
+
disposition: task.checkpoint.disposition
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
status: 'checkpoint_required',
|
|
69
|
+
decision: 'block',
|
|
70
|
+
reason: 'Before finishing, call checkpoint_task_knowledge once with either a bounded durable candidate batch or retain_nothing. Do not store raw transcripts, guesses, or temporary output.'
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
#evictOldest() {
|
|
75
|
+
while (this.sessions.size > MAX_ACTIVE_SESSIONS) {
|
|
76
|
+
const [sessionId, task] = this.sessions.entries().next().value;
|
|
77
|
+
this.sessions.delete(sessionId);
|
|
78
|
+
this.tokens.delete(task.token);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function assertNonEmpty(value, label) {
|
|
84
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
85
|
+
throw new TypeError(`${label} must be a nonempty string`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function sameCheckpoint(left, right) {
|
|
90
|
+
return left.disposition === right.disposition
|
|
91
|
+
&& left.reason === right.reason
|
|
92
|
+
&& left.captureStatus === right.captureStatus;
|
|
93
|
+
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
const READ_TOOLS = new Set([
|
|
2
|
+
'verify_task_checkpoint',
|
|
3
|
+
'search_current_project_knowledge',
|
|
4
|
+
'discover_common_knowledge_candidates',
|
|
5
|
+
'preview_common_knowledge_promotion',
|
|
2
6
|
'list_knowledge_spaces',
|
|
3
7
|
'list_personal_projects',
|
|
4
8
|
'preview_personal_project_action',
|
|
@@ -10,11 +14,16 @@ const READ_TOOLS = new Set([
|
|
|
10
14
|
]);
|
|
11
15
|
|
|
12
16
|
const WRITE_TOOLS = new Set([
|
|
17
|
+
'begin_task_context',
|
|
18
|
+
'checkpoint_task_knowledge',
|
|
13
19
|
'get_collaboration_preferences',
|
|
14
20
|
'resolve_deferred_preference_conflict',
|
|
15
21
|
'capture_session_knowledge',
|
|
22
|
+
'record_decision_trace',
|
|
16
23
|
'search_knowledge_graph',
|
|
17
24
|
'record_knowledge_usage',
|
|
25
|
+
'record_knowledge_feedback',
|
|
26
|
+
'apply_common_knowledge_promotion',
|
|
18
27
|
'get_knowledge_graph',
|
|
19
28
|
'search_human_knowledge_changes',
|
|
20
29
|
'review_human_knowledge_change',
|
|
@@ -34,6 +43,7 @@ const WRITE_TOOLS = new Set([
|
|
|
34
43
|
|
|
35
44
|
const DESTRUCTIVE_TOOLS = new Set([
|
|
36
45
|
'resolve_deferred_preference_conflict',
|
|
46
|
+
'apply_common_knowledge_promotion',
|
|
37
47
|
'review_project_proposal',
|
|
38
48
|
'review_project_relation',
|
|
39
49
|
'review_personal_draft',
|
|
@@ -50,8 +60,13 @@ export function annotationsFor(name) {
|
|
|
50
60
|
destructiveHint: DESTRUCTIVE_TOOLS.has(name),
|
|
51
61
|
idempotentHint: readOnly || name === 'upsert_personal_project' ||
|
|
52
62
|
name === 'publish_personal_project' || name === 'unsubscribe_public_project' ||
|
|
53
|
-
name === 'record_knowledge_usage'
|
|
63
|
+
name === 'record_knowledge_usage' || name === 'record_decision_trace' ||
|
|
64
|
+
name === 'record_knowledge_feedback' ||
|
|
65
|
+
name === 'checkpoint_task_knowledge',
|
|
54
66
|
openWorldHint: name === 'capture_session_knowledge' ||
|
|
67
|
+
name === 'record_decision_trace' ||
|
|
68
|
+
name === 'record_knowledge_feedback' ||
|
|
69
|
+
name === 'checkpoint_task_knowledge' ||
|
|
55
70
|
name === 'publish_personal_project' ||
|
|
56
71
|
name === 'create_project_relation' ||
|
|
57
72
|
name === 'review_project_relation' ||
|
package/src/mcp/tool-result.js
CHANGED
|
@@ -32,6 +32,30 @@ export function successToolResult(value, { limitBytes = RESULT_LIMIT_BYTES } = {
|
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
export function hookAdditionalContextToolResult(
|
|
36
|
+
value,
|
|
37
|
+
{
|
|
38
|
+
hookEventName,
|
|
39
|
+
label,
|
|
40
|
+
limitBytes = RESULT_LIMIT_BYTES
|
|
41
|
+
}
|
|
42
|
+
) {
|
|
43
|
+
const result = successToolResult(value, { limitBytes });
|
|
44
|
+
const additionalContext = `${label}\n${JSON.stringify(result.structuredContent)}`;
|
|
45
|
+
return {
|
|
46
|
+
...result,
|
|
47
|
+
content: [{
|
|
48
|
+
type: 'text',
|
|
49
|
+
text: JSON.stringify({
|
|
50
|
+
hookSpecificOutput: {
|
|
51
|
+
hookEventName,
|
|
52
|
+
additionalContext
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
}]
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
35
59
|
export function errorToolResult(error) {
|
|
36
60
|
const controlled = error instanceof ApplicationError;
|
|
37
61
|
const code = controlled ? error.code : 'internal_error';
|
package/src/runtime-options.js
CHANGED
|
@@ -25,7 +25,7 @@ export function resolveRuntimeOptions(args = [], env = process.env, {
|
|
|
25
25
|
let defaultDbPath = systemDbPath;
|
|
26
26
|
if (cliDbPath === null && envDbPath === null) {
|
|
27
27
|
defaultDbPath = resolveDefaultDbPath({ cwd, existsSync, systemDbPath });
|
|
28
|
-
guardImplicitDefaultUpgrade({ cwd, existsSync
|
|
28
|
+
guardImplicitDefaultUpgrade({ cwd, existsSync });
|
|
29
29
|
}
|
|
30
30
|
return {
|
|
31
31
|
dbPath: cliDbPath ?? envDbPath ?? defaultDbPath,
|
|
@@ -36,8 +36,7 @@ export function resolveRuntimeOptions(args = [], env = process.env, {
|
|
|
36
36
|
|
|
37
37
|
export function resolveStore({ dbPath }) {
|
|
38
38
|
if (extname(dbPath).toLowerCase() === '.json') {
|
|
39
|
-
|
|
40
|
-
throw new RuntimeConfigurationError(migrationMessage(dbPath, destination));
|
|
39
|
+
throw new RuntimeConfigurationError(legacyJsonMessage(dbPath));
|
|
41
40
|
}
|
|
42
41
|
return openSqliteStore(dbPath);
|
|
43
42
|
}
|
|
@@ -59,17 +58,17 @@ function resolveDefaultDbPath({ cwd, existsSync, systemDbPath }) {
|
|
|
59
58
|
return systemDbPath;
|
|
60
59
|
}
|
|
61
60
|
|
|
62
|
-
function guardImplicitDefaultUpgrade({ cwd, existsSync
|
|
61
|
+
function guardImplicitDefaultUpgrade({ cwd, existsSync }) {
|
|
63
62
|
const legacyPath = resolve(cwd, '.fuli/context.json');
|
|
64
63
|
const localDatabasePath = resolve(cwd, LEGACY_LOCAL_DB_PATH);
|
|
65
64
|
if (existsSync(legacyPath) && !existsSync(localDatabasePath)) {
|
|
66
|
-
throw new RuntimeConfigurationError(
|
|
65
|
+
throw new RuntimeConfigurationError(legacyJsonMessage(legacyPath));
|
|
67
66
|
}
|
|
68
67
|
}
|
|
69
68
|
|
|
70
|
-
function
|
|
71
|
-
return
|
|
72
|
-
|
|
69
|
+
function legacyJsonMessage(source) {
|
|
70
|
+
return `JSON is a retired legacy format (${quoteShellArgument(source)}). ` +
|
|
71
|
+
'The current Fuli CLI no longer provides SQLite knowledge commands or JSON migration.';
|
|
73
72
|
}
|
|
74
73
|
|
|
75
74
|
export function openLocalApplication({ dbPath, personalSpaceName = DEFAULT_PERSONAL_SPACE }) {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { isIPv4 } from 'node:net';
|
|
2
|
+
import { networkInterfaces } from 'node:os';
|
|
3
|
+
|
|
4
|
+
export const LAN_ACCESS_USERNAME = 'fuli';
|
|
5
|
+
|
|
6
|
+
export function discoverLanAddresses(interfaces = networkInterfaces()) {
|
|
7
|
+
const addresses = [];
|
|
8
|
+
for (const entries of Object.values(interfaces ?? {})) {
|
|
9
|
+
for (const entry of entries ?? []) {
|
|
10
|
+
if (!entry || entry.internal || !isIpv4Family(entry.family)) continue;
|
|
11
|
+
if (!isPrivateLanIpv4(entry.address)) continue;
|
|
12
|
+
addresses.push(entry.address);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return [...new Set(addresses)].sort(compareIpv4);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function lanConsoleUrls(addresses, port) {
|
|
19
|
+
return addresses.map((address) => `http://${consoleAuthority(address, port)}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function lanServerAuthorities(addresses, port) {
|
|
23
|
+
return lanConsoleUrls(addresses, port).map((url) => new URL(url).host);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isIpv4Family(family) {
|
|
27
|
+
return family === 'IPv4' || family === 4;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isPrivateLanIpv4(address) {
|
|
31
|
+
if (!isIPv4(address)) return false;
|
|
32
|
+
const [first, second] = address.split('.').map(Number);
|
|
33
|
+
return first === 10 ||
|
|
34
|
+
(first === 172 && second >= 16 && second <= 31) ||
|
|
35
|
+
(first === 192 && second === 168) ||
|
|
36
|
+
(first === 169 && second === 254);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function consoleAuthority(address, port) {
|
|
40
|
+
return Number(port) === 80 ? address : `${address}:${port}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function compareIpv4(left, right) {
|
|
44
|
+
const leftParts = left.split('.').map(Number);
|
|
45
|
+
const rightParts = right.split('.').map(Number);
|
|
46
|
+
for (let index = 0; index < 4; index += 1) {
|
|
47
|
+
if (leftParts[index] !== rightParts[index]) {
|
|
48
|
+
return leftParts[index] - rightParts[index];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
package/src/server.js
CHANGED
|
@@ -4,11 +4,16 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
|
|
5
5
|
import { handleApiRequest } from './http/api-router.js';
|
|
6
6
|
import { mapHttpError } from './http/error-mapping.js';
|
|
7
|
-
import { localServerAuthority,
|
|
7
|
+
import { localServerAuthority, rejectRequestOutsidePolicy } from './http/request-policy.js';
|
|
8
8
|
import { sendJson } from './http/response.js';
|
|
9
9
|
import { serveStatic } from './http/static-handler.js';
|
|
10
10
|
import { DEFAULT_FULI_PORT } from './defaults.js';
|
|
11
11
|
import { resolveGraphRuntimeOptions } from './graphiti/runtime-config.js';
|
|
12
|
+
import {
|
|
13
|
+
discoverLanAddresses,
|
|
14
|
+
lanConsoleUrls,
|
|
15
|
+
lanServerAuthorities
|
|
16
|
+
} from './server/lan-access.js';
|
|
12
17
|
import { createServerApplication } from './server/application-lifecycle.js';
|
|
13
18
|
import { listenServer } from './server/listen.js';
|
|
14
19
|
|
|
@@ -26,8 +31,19 @@ export async function createServer(options = {}) {
|
|
|
26
31
|
store,
|
|
27
32
|
app,
|
|
28
33
|
closeApplicationOnShutdown,
|
|
29
|
-
isBlockedPort = isFetchBlockedPort
|
|
34
|
+
isBlockedPort = isFetchBlockedPort,
|
|
35
|
+
lan = false,
|
|
36
|
+
lanAccessToken = null,
|
|
37
|
+
lanAddresses: configuredLanAddresses = null
|
|
30
38
|
} = options;
|
|
39
|
+
const lanEnabled = lan === true;
|
|
40
|
+
const lanToken = lanEnabled ? requiredLanToken(lanAccessToken) : null;
|
|
41
|
+
const lanAddresses = lanEnabled
|
|
42
|
+
? [...new Set(configuredLanAddresses ?? discoverLanAddresses())]
|
|
43
|
+
: [];
|
|
44
|
+
if (lanEnabled && lanAddresses.length === 0) {
|
|
45
|
+
throw new Error('No private IPv4 LAN address is available');
|
|
46
|
+
}
|
|
31
47
|
if (Number(port) !== 0 && isBlockedPort(port)) {
|
|
32
48
|
throw new Error(`Port ${port} is blocked for local fetch`);
|
|
33
49
|
}
|
|
@@ -45,15 +61,23 @@ export async function createServer(options = {}) {
|
|
|
45
61
|
|
|
46
62
|
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
47
63
|
let authority = null;
|
|
64
|
+
let allowedLanAuthorities = [];
|
|
48
65
|
const server = createHttpServer((request, response) => {
|
|
49
|
-
handleRequest({
|
|
66
|
+
handleRequest({
|
|
67
|
+
request,
|
|
68
|
+
response,
|
|
69
|
+
app: application,
|
|
70
|
+
authority,
|
|
71
|
+
lanAuthorities: allowedLanAuthorities,
|
|
72
|
+
lanAccessToken: lanToken
|
|
73
|
+
}).catch((error) => {
|
|
50
74
|
const mapped = mapHttpError(error);
|
|
51
75
|
sendJson(response, mapped.status, mapped.body);
|
|
52
76
|
});
|
|
53
77
|
});
|
|
54
78
|
|
|
55
79
|
try {
|
|
56
|
-
await listenServer(server, port);
|
|
80
|
+
await listenServer(server, port, lanEnabled ? '0.0.0.0' : '127.0.0.1');
|
|
57
81
|
} catch (error) {
|
|
58
82
|
runtime.close();
|
|
59
83
|
throw error;
|
|
@@ -66,6 +90,9 @@ export async function createServer(options = {}) {
|
|
|
66
90
|
throw new Error(`Port ${address.port} is blocked for local fetch`);
|
|
67
91
|
}
|
|
68
92
|
authority = localServerAuthority(address);
|
|
93
|
+
allowedLanAuthorities = lanEnabled
|
|
94
|
+
? lanServerAuthorities(lanAddresses, address.port)
|
|
95
|
+
: [];
|
|
69
96
|
if (!authority) {
|
|
70
97
|
await new Promise((resolveClose) => server.close(resolveClose));
|
|
71
98
|
runtime.close();
|
|
@@ -73,7 +100,12 @@ export async function createServer(options = {}) {
|
|
|
73
100
|
}
|
|
74
101
|
|
|
75
102
|
server.once('close', runtime.close);
|
|
76
|
-
return {
|
|
103
|
+
return {
|
|
104
|
+
server,
|
|
105
|
+
url: `http://${authority}`,
|
|
106
|
+
lanUrls: lanEnabled ? lanConsoleUrls(lanAddresses, address.port) : [],
|
|
107
|
+
app: application
|
|
108
|
+
};
|
|
77
109
|
}
|
|
78
110
|
|
|
79
111
|
runtime.close();
|
|
@@ -94,8 +126,21 @@ export function isFetchBlockedPort(port) {
|
|
|
94
126
|
return FETCH_BLOCKED_PORTS.has(Number(port));
|
|
95
127
|
}
|
|
96
128
|
|
|
97
|
-
async function handleRequest({
|
|
98
|
-
|
|
129
|
+
async function handleRequest({
|
|
130
|
+
request,
|
|
131
|
+
response,
|
|
132
|
+
app,
|
|
133
|
+
authority,
|
|
134
|
+
lanAuthorities,
|
|
135
|
+
lanAccessToken
|
|
136
|
+
}) {
|
|
137
|
+
if (rejectRequestOutsidePolicy({
|
|
138
|
+
request,
|
|
139
|
+
response,
|
|
140
|
+
authority,
|
|
141
|
+
lanAuthorities,
|
|
142
|
+
lanAccessToken
|
|
143
|
+
})) return;
|
|
99
144
|
if (await handleApiRequest({ request, response, app })) return;
|
|
100
145
|
serveStatic(new URL(request.url, 'http://127.0.0.1').pathname, response);
|
|
101
146
|
}
|
|
@@ -110,7 +155,13 @@ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
|
110
155
|
async function runProgram(args) {
|
|
111
156
|
const runtimeOptions = resolveGraphRuntimeOptions(args, process.env);
|
|
112
157
|
const port = numberOption(args, '--port', DEFAULT_FULI_PORT);
|
|
113
|
-
const
|
|
158
|
+
const lan = args.includes('--lan');
|
|
159
|
+
const { server, url, lanUrls } = await createServer({
|
|
160
|
+
...runtimeOptions,
|
|
161
|
+
port,
|
|
162
|
+
lan,
|
|
163
|
+
lanAccessToken: lan ? process.env.FULI_LAN_ACCESS_TOKEN : null
|
|
164
|
+
});
|
|
114
165
|
const handlers = new Map();
|
|
115
166
|
const close = () => server.close();
|
|
116
167
|
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
@@ -122,6 +173,14 @@ async function runProgram(args) {
|
|
|
122
173
|
for (const [signal, handler] of handlers) process.off(signal, handler);
|
|
123
174
|
});
|
|
124
175
|
console.log(`复利工作台 running at ${url}`);
|
|
176
|
+
for (const lanUrl of lanUrls) console.log(`复利工作台 LAN at ${lanUrl}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function requiredLanToken(value) {
|
|
180
|
+
if (typeof value !== 'string' || value.length < 16) {
|
|
181
|
+
throw new TypeError('LAN mode requires a generated access token');
|
|
182
|
+
}
|
|
183
|
+
return value;
|
|
125
184
|
}
|
|
126
185
|
|
|
127
186
|
function numberOption(args, flag, fallback) {
|
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
2
2
|
|
|
3
3
|
import { replaceFuliTable } from './codex-config.js';
|
|
4
4
|
import { isCodexBootstrapCurrent } from './codex-bootstrap.js';
|
|
5
|
+
import { hasCurrentClaudeCodeHooks } from './claude-code-config.js';
|
|
5
6
|
import { isSessionSkillCurrent } from './session-skill.js';
|
|
6
7
|
import { readJsonFile } from '../storage/json-file.js';
|
|
7
8
|
|
|
@@ -27,9 +28,16 @@ export function inspectAgentInstallations(agents, context, {
|
|
|
27
28
|
context,
|
|
28
29
|
{ fileExists, readText }
|
|
29
30
|
);
|
|
31
|
+
const claudeLifecycleCurrent = agent.id !== 'claude-code' ||
|
|
32
|
+
hasCurrentClaudeCodeHooks(readJson(agent.settingsPath));
|
|
30
33
|
|
|
31
34
|
let integrationStatus = 'not_connected';
|
|
32
|
-
if (
|
|
35
|
+
if (
|
|
36
|
+
registration.current
|
|
37
|
+
&& skillsCurrent
|
|
38
|
+
&& codexBootstrapCurrent
|
|
39
|
+
&& claudeLifecycleCurrent
|
|
40
|
+
) {
|
|
33
41
|
integrationStatus = 'connected';
|
|
34
42
|
}
|
|
35
43
|
else if (registration.present || skillsPresent) integrationStatus = 'update_available';
|
|
@@ -42,6 +50,9 @@ export function inspectAgentInstallations(agents, context, {
|
|
|
42
50
|
skills: skillsCurrent ? 'current' : skillsPresent ? 'outdated' : 'missing',
|
|
43
51
|
...(agent.id === 'codex'
|
|
44
52
|
? { bootstrap: codexBootstrapCurrent ? 'current' : 'outdated' }
|
|
53
|
+
: {}),
|
|
54
|
+
...(agent.id === 'claude-code'
|
|
55
|
+
? { lifecycleHooks: claudeLifecycleCurrent ? 'current' : 'outdated' }
|
|
45
56
|
: {})
|
|
46
57
|
}
|
|
47
58
|
};
|
|
@@ -71,7 +82,8 @@ function inspectRegistration(agent, context, readers) {
|
|
|
71
82
|
present,
|
|
72
83
|
current: present &&
|
|
73
84
|
server.command === expected.command &&
|
|
74
|
-
sameStrings(server.args, expected.args)
|
|
85
|
+
sameStrings(server.args, expected.args) &&
|
|
86
|
+
(agent.id !== 'claude-code' || server.alwaysLoad === true)
|
|
75
87
|
};
|
|
76
88
|
} catch {
|
|
77
89
|
return { present: false, current: false };
|
package/src/setup/agents.js
CHANGED
|
@@ -3,6 +3,10 @@ import { existsSync } from 'node:fs';
|
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
|
|
6
|
+
import {
|
|
7
|
+
connectClaudeCode,
|
|
8
|
+
disconnectClaudeCode
|
|
9
|
+
} from './claude-code-config.js';
|
|
6
10
|
import { connectCursor, disconnectCursor } from './cursor-config.js';
|
|
7
11
|
import { connectCodex, disconnectCodex } from './codex-config.js';
|
|
8
12
|
|
|
@@ -34,6 +38,7 @@ export function discoverAgents({
|
|
|
34
38
|
label: 'Claude Code',
|
|
35
39
|
command: 'claude',
|
|
36
40
|
configPath: pathApi.join(homeDir, '.claude.json'),
|
|
41
|
+
settingsPath: pathApi.join(homeDir, '.claude', 'settings.json'),
|
|
37
42
|
skillPath: pathApi.join(homeDir, '.claude', 'skills', skillName),
|
|
38
43
|
projectSkillPath: pathApi.join(homeDir, '.claude', 'skills', projectSkillName),
|
|
39
44
|
available: Boolean(commandExists('claude'))
|
|
@@ -75,9 +80,13 @@ export function buildAgentCommands(agent, context) {
|
|
|
75
80
|
export function connectAgent(agent, context, {
|
|
76
81
|
runCommand = defaultRunCommand,
|
|
77
82
|
connectCodexConfig = connectCodex,
|
|
83
|
+
connectClaudeCodeConfig = connectClaudeCode,
|
|
78
84
|
connectCursorConfig = connectCursor
|
|
79
85
|
} = {}) {
|
|
80
86
|
if (agent.id === 'codex') return connectCodexConfig(agent, context);
|
|
87
|
+
if (agent.id === 'claude-code') {
|
|
88
|
+
return connectClaudeCodeConfig(agent, context);
|
|
89
|
+
}
|
|
81
90
|
if (agent.id === 'cursor') return connectCursorConfig(agent, context);
|
|
82
91
|
|
|
83
92
|
const commands = buildAgentCommands(agent, context);
|
|
@@ -91,10 +100,12 @@ export function connectAgent(agent, context, {
|
|
|
91
100
|
|
|
92
101
|
export function disconnectAgent(agent, {
|
|
93
102
|
disconnectCodexConfig = disconnectCodex,
|
|
103
|
+
disconnectClaudeCodeConfig = disconnectClaudeCode,
|
|
94
104
|
disconnectJsonConfig = disconnectCursor
|
|
95
105
|
} = {}) {
|
|
96
106
|
if (agent.id === 'codex') return disconnectCodexConfig(agent);
|
|
97
|
-
if (agent.id === '
|
|
107
|
+
if (agent.id === 'claude-code') return disconnectClaudeCodeConfig(agent);
|
|
108
|
+
if (agent.id === 'cursor') {
|
|
98
109
|
return disconnectJsonConfig(agent);
|
|
99
110
|
}
|
|
100
111
|
throw new TypeError(`Unsupported agent: ${agent.id}`);
|