gnosys 5.5.0 → 5.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 +44 -0
- package/dist/cli.js +204 -18
- package/dist/cli.js.map +1 -1
- package/dist/lib/chat/choose.d.ts +75 -0
- package/dist/lib/chat/choose.d.ts.map +1 -0
- package/dist/lib/chat/choose.js +146 -0
- package/dist/lib/chat/choose.js.map +1 -0
- package/dist/lib/chat/commands.d.ts +96 -0
- package/dist/lib/chat/commands.d.ts.map +1 -0
- package/dist/lib/chat/commands.js +367 -0
- package/dist/lib/chat/commands.js.map +1 -0
- package/dist/lib/chat/focus.d.ts +70 -0
- package/dist/lib/chat/focus.d.ts.map +1 -0
- package/dist/lib/chat/focus.js +120 -0
- package/dist/lib/chat/focus.js.map +1 -0
- package/dist/lib/chat/index.d.ts +32 -0
- package/dist/lib/chat/index.d.ts.map +1 -0
- package/dist/lib/chat/index.js +151 -0
- package/dist/lib/chat/index.js.map +1 -0
- package/dist/lib/chat/intent.d.ts +100 -0
- package/dist/lib/chat/intent.d.ts.map +1 -0
- package/dist/lib/chat/intent.js +192 -0
- package/dist/lib/chat/intent.js.map +1 -0
- package/dist/lib/chat/llmTurn.d.ts +37 -0
- package/dist/lib/chat/llmTurn.d.ts.map +1 -0
- package/dist/lib/chat/llmTurn.js +61 -0
- package/dist/lib/chat/llmTurn.js.map +1 -0
- package/dist/lib/chat/recall.d.ts +58 -0
- package/dist/lib/chat/recall.d.ts.map +1 -0
- package/dist/lib/chat/recall.js +109 -0
- package/dist/lib/chat/recall.js.map +1 -0
- package/dist/lib/chat/render.d.ts +30 -0
- package/dist/lib/chat/render.d.ts.map +1 -0
- package/dist/lib/chat/render.js +737 -0
- package/dist/lib/chat/render.js.map +1 -0
- package/dist/lib/chat/session.d.ts +121 -0
- package/dist/lib/chat/session.d.ts.map +1 -0
- package/dist/lib/chat/session.js +148 -0
- package/dist/lib/chat/session.js.map +1 -0
- package/dist/lib/chat/types.d.ts +42 -0
- package/dist/lib/chat/types.d.ts.map +1 -0
- package/dist/lib/chat/types.js +6 -0
- package/dist/lib/chat/types.js.map +1 -0
- package/dist/lib/chat/write.d.ts +66 -0
- package/dist/lib/chat/write.d.ts.map +1 -0
- package/dist/lib/chat/write.js +203 -0
- package/dist/lib/chat/write.js.map +1 -0
- package/dist/lib/db.d.ts +3 -1
- package/dist/lib/db.d.ts.map +1 -1
- package/dist/lib/db.js +18 -2
- package/dist/lib/db.js.map +1 -1
- package/dist/lib/exportProject.d.ts +51 -0
- package/dist/lib/exportProject.d.ts.map +1 -0
- package/dist/lib/exportProject.js +72 -0
- package/dist/lib/exportProject.js.map +1 -0
- package/dist/lib/importProject.d.ts +35 -0
- package/dist/lib/importProject.d.ts.map +1 -0
- package/dist/lib/importProject.js +135 -0
- package/dist/lib/importProject.js.map +1 -0
- package/package.json +7 -1
package/README.md
CHANGED
|
@@ -96,6 +96,50 @@ The helper auto-starts the sandbox if it's not running. No MCP required.
|
|
|
96
96
|
|
|
97
97
|
---
|
|
98
98
|
|
|
99
|
+
## Interactive Chat (TUI)
|
|
100
|
+
|
|
101
|
+
`gnosys chat` opens a memory-aware terminal chat. Every prompt triggers federated recall against the central brain; the LLM sees relevant memories in context and cites them in its answers.
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
gnosys chat # new session
|
|
105
|
+
gnosys chat --resume <id> # continue an earlier session
|
|
106
|
+
gnosys chat --list # see all sessions
|
|
107
|
+
gnosys chat --search <query> # full-text search across session logs
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Free-text or slash commands.** "remember that flag default is OFF" works the same as `/remember flag default is OFF`. The TUI also recognizes "what did we decide about ULIDs?" → `/recall`, "thanks, that's all" → `/quit`. Destructive intents always confirm; non-destructive ones auto-accept after 5 confirmations of the same pattern.
|
|
111
|
+
|
|
112
|
+
**24 slash commands** across reading, recall, writing, focus, and polish — type `/help` inside the TUI for the full list. Highlights:
|
|
113
|
+
|
|
114
|
+
- `/pin <id>`, `/scope`, `/threshold`, `/recall <q>` — tune what shows up in context
|
|
115
|
+
- `/remember <text>`, `/save-turn`, `/attach <file>` — promote chat content to gnosys memory (PDFs, images, audio all auto-pin to the session)
|
|
116
|
+
- `/focus <topic>`, `/branch`, `/resume-focus` — replace the "new chat" model with cheap focus boundaries; one continuous session log, instant pivot
|
|
117
|
+
- `/export <file.md>`, `/search-chats <q>`, `/dream-here` — round-trip the session, find old chats, or trigger a focused dream cycle
|
|
118
|
+
|
|
119
|
+
**Multiple choice.** When the model needs you to pick from a small set, it emits a fenced `gnosys-choose` block. The TUI parses it and shows an arrow-key selectable list — provider-agnostic, no tool-use API required.
|
|
120
|
+
|
|
121
|
+
Sessions live as append-only JSONL at `~/.gnosys/chat-sessions/`; promoted memories carry `session:<id>`, `from-chat:true`, and `source:remember|save-turn|auto|attach` provenance tags so you can find them later via federated search.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Per-Project Bundles
|
|
126
|
+
|
|
127
|
+
Move a single project's memories between machines without dragging the whole central DB.
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
gnosys export project --to ./gnosys-public.json.gz # auto-detects current project
|
|
131
|
+
gnosys export project <projectId> --to <bundle> # explicit
|
|
132
|
+
gnosys import project <bundle> --strategy merge # default — skip existing
|
|
133
|
+
gnosys import project <bundle> --strategy replace # wipe target project first
|
|
134
|
+
gnosys import project <bundle> --strategy new-id # remap to a fresh project ID
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Bundles are gzipped JSON containing the project row, memories (with embeddings inline), relationships, and audit log. Lossless round-trip with the same DB schema; partially compatible across versions via the bundle manifest.
|
|
138
|
+
|
|
139
|
+
For an Obsidian-compatible markdown vault, use `gnosys export vault --to <dir>` (the v5.5.x form `gnosys export --to <dir>` keeps working).
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
99
143
|
## Web Knowledge Base
|
|
100
144
|
|
|
101
145
|
Turn any website into a searchable knowledge base for AI chatbots. No database required. Works on Vercel, Netlify, Cloudflare Pages, or any platform that can serve files.
|
package/dist/cli.js
CHANGED
|
@@ -108,6 +108,32 @@ function maybePrintUpgradeNudge() {
|
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
maybePrintUpgradeNudge();
|
|
111
|
+
/**
|
|
112
|
+
* v5.6.0 back-compat shim: rewrite `gnosys export --to <dir>` →
|
|
113
|
+
* `gnosys export vault --to <dir>` before commander parses argv. The v5.6.0
|
|
114
|
+
* restructure made `export` a parent command with `vault` and `project`
|
|
115
|
+
* subcommands; without this shim, the bare `--to` form prints usage instead
|
|
116
|
+
* of running the vault export.
|
|
117
|
+
*
|
|
118
|
+
* Pattern: argv[2]==="export" AND argv[3] is not a known subcommand AND any
|
|
119
|
+
* of the v5.5.x flags appear (`--to`, `--all`, `--overwrite`, etc.).
|
|
120
|
+
*/
|
|
121
|
+
function rewriteLegacyExport() {
|
|
122
|
+
if (process.argv[2] !== "export")
|
|
123
|
+
return;
|
|
124
|
+
const next = process.argv[3];
|
|
125
|
+
if (next === "vault" || next === "project" || next === "--help" || next === "-h")
|
|
126
|
+
return;
|
|
127
|
+
// Any v5.5.x-style flag → assume legacy vault invocation
|
|
128
|
+
const looksLegacy = process.argv.slice(3).some((a) => a === "--to" || a.startsWith("--to=") ||
|
|
129
|
+
a === "--all" || a === "--overwrite" ||
|
|
130
|
+
a === "--no-summaries" || a === "--no-reviews" || a === "--no-graph" ||
|
|
131
|
+
a === "--json");
|
|
132
|
+
if (looksLegacy) {
|
|
133
|
+
process.argv.splice(3, 0, "vault");
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
rewriteLegacyExport();
|
|
111
137
|
async function getResolver() {
|
|
112
138
|
const resolver = new GnosysResolver();
|
|
113
139
|
await resolver.resolve();
|
|
@@ -1218,6 +1244,45 @@ program
|
|
|
1218
1244
|
centralDb?.close();
|
|
1219
1245
|
}
|
|
1220
1246
|
});
|
|
1247
|
+
// ─── gnosys chat (TUI) ───────────────────────────────────────────────────
|
|
1248
|
+
program
|
|
1249
|
+
.command("chat")
|
|
1250
|
+
.description("Interactive memory-aware terminal chat (TUI)")
|
|
1251
|
+
.option("--resume <sessionId>", "Resume an existing chat session")
|
|
1252
|
+
.option("--list", "List recent chat sessions and exit")
|
|
1253
|
+
.option("--search <query>", "Full-text search across session logs")
|
|
1254
|
+
.option("--provider <name>", "Override LLM provider (anthropic, openai, groq, ollama, …)")
|
|
1255
|
+
.option("--model <name>", "Override LLM model name")
|
|
1256
|
+
.option("--limit <n>", "Limit for --list / --search (default 20)", "20")
|
|
1257
|
+
.action(async (opts) => {
|
|
1258
|
+
const limit = parseInt(opts.limit, 10) || 20;
|
|
1259
|
+
const chat = await import("./lib/chat/index.js");
|
|
1260
|
+
if (opts.list) {
|
|
1261
|
+
chat.printSessionList(limit);
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
if (opts.search) {
|
|
1265
|
+
chat.printSearchResults(opts.search, limit);
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
// Interactive chat
|
|
1269
|
+
const resolver = await getResolver();
|
|
1270
|
+
const stores = resolver.getStores();
|
|
1271
|
+
const storePath = stores[0]?.path ?? process.cwd();
|
|
1272
|
+
let cliConfig;
|
|
1273
|
+
try {
|
|
1274
|
+
cliConfig = await loadConfig(storePath);
|
|
1275
|
+
}
|
|
1276
|
+
catch {
|
|
1277
|
+
cliConfig = (await import("./lib/config.js")).DEFAULT_CONFIG;
|
|
1278
|
+
}
|
|
1279
|
+
await chat.startChat({
|
|
1280
|
+
config: cliConfig,
|
|
1281
|
+
resume: opts.resume,
|
|
1282
|
+
providerName: opts.provider,
|
|
1283
|
+
modelName: opts.model,
|
|
1284
|
+
});
|
|
1285
|
+
});
|
|
1221
1286
|
// ─── gnosys ingest <file> ─────────────────────────────────────────────────
|
|
1222
1287
|
program
|
|
1223
1288
|
.command("ingest <fileOrGlob>")
|
|
@@ -1876,12 +1941,13 @@ program
|
|
|
1876
1941
|
}
|
|
1877
1942
|
}
|
|
1878
1943
|
});
|
|
1879
|
-
// ─── gnosys import
|
|
1880
|
-
program
|
|
1881
|
-
.command("import
|
|
1882
|
-
.
|
|
1883
|
-
.
|
|
1884
|
-
.
|
|
1944
|
+
// ─── gnosys import (parent + subcommands) ───────────────────────────────
|
|
1945
|
+
const importCmd = program
|
|
1946
|
+
.command("import [fileOrUrl]")
|
|
1947
|
+
.enablePositionalOptions()
|
|
1948
|
+
.description("Import data into Gnosys (bulk CSV/JSON/JSONL — see also: 'gnosys import project <bundle>')")
|
|
1949
|
+
.option("--format <format>", "Data format: csv, json, jsonl (required for bulk import)")
|
|
1950
|
+
.option("--mapping <json>", 'Field mapping as JSON: \'{"source_field":"gnosys_field"}\'. Valid targets: title, category, content, tags, relevance')
|
|
1885
1951
|
.option("--mode <mode>", "Processing mode: llm or structured", "structured")
|
|
1886
1952
|
.option("--limit <n>", "Max records to import", parseInt)
|
|
1887
1953
|
.option("--offset <n>", "Skip first N records", parseInt)
|
|
@@ -1892,6 +1958,17 @@ program
|
|
|
1892
1958
|
.option("--dry-run", "Preview without writing")
|
|
1893
1959
|
.option("--store <store>", "Target store: project, personal, global", "project")
|
|
1894
1960
|
.action(async (fileOrUrl, opts) => {
|
|
1961
|
+
if (!fileOrUrl) {
|
|
1962
|
+
console.error("Usage:");
|
|
1963
|
+
console.error(" gnosys import <file> --format csv|json|jsonl --mapping '{...}' (bulk)");
|
|
1964
|
+
console.error(" gnosys import project <bundle.json.gz> (project bundle)");
|
|
1965
|
+
process.exit(1);
|
|
1966
|
+
}
|
|
1967
|
+
if (!opts.format || !opts.mapping) {
|
|
1968
|
+
console.error("Error: --format and --mapping are required for bulk imports.");
|
|
1969
|
+
console.error(" For project bundles, use 'gnosys import project <bundle>'.");
|
|
1970
|
+
process.exit(1);
|
|
1971
|
+
}
|
|
1895
1972
|
// Parse mapping JSON
|
|
1896
1973
|
let mapping;
|
|
1897
1974
|
try {
|
|
@@ -1966,6 +2043,51 @@ program
|
|
|
1966
2043
|
process.exit(1);
|
|
1967
2044
|
}
|
|
1968
2045
|
});
|
|
2046
|
+
// `gnosys import project <bundle>` — restore a portable .json.gz bundle
|
|
2047
|
+
importCmd
|
|
2048
|
+
.command("project <bundlePath>")
|
|
2049
|
+
.description("Import a project bundle (.json.gz) created by 'gnosys export project'")
|
|
2050
|
+
.option("--strategy <strategy>", "Conflict handling: merge (default), replace, new-id", "merge")
|
|
2051
|
+
.option("--working-directory <dir>", "Override the bundle's working_directory (e.g. when restoring on a different machine)")
|
|
2052
|
+
.option("--json", "Output the result as JSON")
|
|
2053
|
+
.action(async (bundlePath, opts) => {
|
|
2054
|
+
const validStrategies = ["merge", "replace", "new-id"];
|
|
2055
|
+
if (!validStrategies.includes(opts.strategy)) {
|
|
2056
|
+
console.error(`Invalid strategy: ${opts.strategy}. Use one of: ${validStrategies.join(", ")}`);
|
|
2057
|
+
process.exit(1);
|
|
2058
|
+
}
|
|
2059
|
+
const { GnosysDB: DbClass } = await import("./lib/db.js");
|
|
2060
|
+
const { importProject } = await import("./lib/importProject.js");
|
|
2061
|
+
const centralDb = DbClass.openCentral();
|
|
2062
|
+
if (!centralDb.isAvailable()) {
|
|
2063
|
+
console.error("Central DB unavailable.");
|
|
2064
|
+
process.exit(1);
|
|
2065
|
+
}
|
|
2066
|
+
try {
|
|
2067
|
+
const result = importProject(centralDb, {
|
|
2068
|
+
bundlePath: path.resolve(bundlePath),
|
|
2069
|
+
strategy: opts.strategy,
|
|
2070
|
+
workingDirectoryOverride: opts.workingDirectory,
|
|
2071
|
+
});
|
|
2072
|
+
if (opts.json) {
|
|
2073
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2074
|
+
}
|
|
2075
|
+
else {
|
|
2076
|
+
console.log(`Imported project ${result.projectName} (${result.projectId})`);
|
|
2077
|
+
console.log(` Strategy: ${result.strategy}`);
|
|
2078
|
+
console.log(` Memories: ${result.memoriesInserted} inserted, ${result.memoriesSkipped} skipped, ${result.memoriesReplaced} replaced`);
|
|
2079
|
+
console.log(` Relationships: ${result.relationshipsInserted}`);
|
|
2080
|
+
console.log(` Audit entries: ${result.auditEntriesInserted}`);
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
catch (err) {
|
|
2084
|
+
console.error(`Import failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2085
|
+
process.exit(1);
|
|
2086
|
+
}
|
|
2087
|
+
finally {
|
|
2088
|
+
centralDb.close();
|
|
2089
|
+
}
|
|
2090
|
+
});
|
|
1969
2091
|
// ─── gnosys reindex ──────────────────────────────────────────────────────
|
|
1970
2092
|
program
|
|
1971
2093
|
.command("reindex")
|
|
@@ -3495,18 +3617,7 @@ dreamCmd
|
|
|
3495
3617
|
centralDb?.close();
|
|
3496
3618
|
}
|
|
3497
3619
|
});
|
|
3498
|
-
|
|
3499
|
-
program
|
|
3500
|
-
.command("export")
|
|
3501
|
-
.description("Export gnosys.db to Obsidian-compatible vault (one-way)")
|
|
3502
|
-
.requiredOption("--to <dir>", "Target directory for export")
|
|
3503
|
-
.option("--all", "Export all memories (active + archived)")
|
|
3504
|
-
.option("--overwrite", "Overwrite existing files")
|
|
3505
|
-
.option("--no-summaries", "Skip category summaries")
|
|
3506
|
-
.option("--no-reviews", "Skip review suggestions")
|
|
3507
|
-
.option("--no-graph", "Skip relationship graph")
|
|
3508
|
-
.option("--json", "Output raw JSON report")
|
|
3509
|
-
.action(async (opts) => {
|
|
3620
|
+
async function runVaultExport(opts) {
|
|
3510
3621
|
const resolver = new GnosysResolver();
|
|
3511
3622
|
await resolver.resolve();
|
|
3512
3623
|
const stores = resolver.getStores();
|
|
@@ -3545,6 +3656,81 @@ program
|
|
|
3545
3656
|
console.log(formatExportReport(report));
|
|
3546
3657
|
}
|
|
3547
3658
|
db.close();
|
|
3659
|
+
}
|
|
3660
|
+
const exportCmd = program
|
|
3661
|
+
.command("export")
|
|
3662
|
+
.description("Export memory to a vault (markdown) or a project bundle (.json.gz)")
|
|
3663
|
+
.enablePositionalOptions();
|
|
3664
|
+
// Bare `gnosys export` shows the canonical subcommand forms. Back-compat for
|
|
3665
|
+
// the v5.5.x form `gnosys export --to <dir>` is handled in a pre-parse shim
|
|
3666
|
+
// at the top of the file (rewrites argv to insert "vault" before "--to").
|
|
3667
|
+
exportCmd.action(() => {
|
|
3668
|
+
console.error("Usage: gnosys export vault --to <dir> # Obsidian vault export");
|
|
3669
|
+
console.error(" gnosys export project [id] --to <bundle> # portable .json.gz bundle");
|
|
3670
|
+
process.exit(1);
|
|
3671
|
+
});
|
|
3672
|
+
// `gnosys export vault` — explicit alias for the default behavior
|
|
3673
|
+
exportCmd
|
|
3674
|
+
.command("vault")
|
|
3675
|
+
.description("Export gnosys.db to an Obsidian-compatible vault (one-way)")
|
|
3676
|
+
.requiredOption("--to <dir>", "Target directory for export")
|
|
3677
|
+
.option("--all", "Export all memories (active + archived)")
|
|
3678
|
+
.option("--overwrite", "Overwrite existing files")
|
|
3679
|
+
.option("--no-summaries", "Skip category summaries")
|
|
3680
|
+
.option("--no-reviews", "Skip review suggestions")
|
|
3681
|
+
.option("--no-graph", "Skip relationship graph")
|
|
3682
|
+
.option("--json", "Output raw JSON report")
|
|
3683
|
+
.action(runVaultExport);
|
|
3684
|
+
// `gnosys export project [id]` — bundle a single project for portability
|
|
3685
|
+
exportCmd
|
|
3686
|
+
.command("project [projectId]")
|
|
3687
|
+
.description("Export a single project to a portable .json.gz bundle (round-trips with 'gnosys import project')")
|
|
3688
|
+
.requiredOption("--to <file>", "Output bundle file path (e.g. ./gnosys-public.gnosys.json.gz)")
|
|
3689
|
+
.option("--include-archived", "Include archived and superseded memories (default: active only)")
|
|
3690
|
+
.option("--no-audit", "Skip the audit log")
|
|
3691
|
+
.option("--json", "Output the result as JSON")
|
|
3692
|
+
.action(async (projectIdArg, opts) => {
|
|
3693
|
+
const { GnosysDB: DbClass } = await import("./lib/db.js");
|
|
3694
|
+
const { exportProject } = await import("./lib/exportProject.js");
|
|
3695
|
+
const centralDb = DbClass.openCentral();
|
|
3696
|
+
if (!centralDb.isAvailable()) {
|
|
3697
|
+
console.error("Central DB unavailable.");
|
|
3698
|
+
process.exit(1);
|
|
3699
|
+
}
|
|
3700
|
+
let projectId = projectIdArg;
|
|
3701
|
+
if (!projectId) {
|
|
3702
|
+
// Auto-detect from cwd
|
|
3703
|
+
const proj = centralDb.getProjectByDirectory(process.cwd());
|
|
3704
|
+
if (!proj) {
|
|
3705
|
+
console.error("No project ID given and current directory is not a registered project.");
|
|
3706
|
+
console.error("Usage: gnosys export project <projectId> --to <file>");
|
|
3707
|
+
process.exit(1);
|
|
3708
|
+
}
|
|
3709
|
+
projectId = proj.id;
|
|
3710
|
+
}
|
|
3711
|
+
try {
|
|
3712
|
+
const result = exportProject(centralDb, {
|
|
3713
|
+
projectId,
|
|
3714
|
+
outputPath: path.resolve(opts.to),
|
|
3715
|
+
includeArchived: !!opts.includeArchived,
|
|
3716
|
+
includeAudit: opts.audit !== false,
|
|
3717
|
+
});
|
|
3718
|
+
if (opts.json) {
|
|
3719
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3720
|
+
}
|
|
3721
|
+
else {
|
|
3722
|
+
const ratio = (result.compressedBytes / result.uncompressedBytes * 100).toFixed(1);
|
|
3723
|
+
console.log(`Exported project ${projectId}`);
|
|
3724
|
+
console.log(` Memories: ${result.memoryCount}`);
|
|
3725
|
+
console.log(` Relationships: ${result.relationshipCount}`);
|
|
3726
|
+
console.log(` Audit entries: ${result.auditEntryCount}`);
|
|
3727
|
+
console.log(` Bundle: ${result.outputPath}`);
|
|
3728
|
+
console.log(` Size: ${(result.compressedBytes / 1024).toFixed(1)} KB compressed (${ratio}% of ${(result.uncompressedBytes / 1024).toFixed(1)} KB)`);
|
|
3729
|
+
}
|
|
3730
|
+
}
|
|
3731
|
+
finally {
|
|
3732
|
+
centralDb.close();
|
|
3733
|
+
}
|
|
3548
3734
|
});
|
|
3549
3735
|
// ─── gnosys serve ────────────────────────────────────────────────────────
|
|
3550
3736
|
program
|