@memories.sh/cli 0.2.2 → 0.3.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/{chunk-BDPB4ABQ.js → chunk-GCWB2OVN.js} +22 -0
- package/dist/{chunk-JMN3U7AI.js → chunk-TRACL3XY.js} +2 -2
- package/dist/{chunk-4GNYCHD4.js → chunk-ZSP6BMAR.js} +1 -1
- package/dist/{embeddings-GH4HB6X7.js → embeddings-6N2NZPRX.js} +2 -2
- package/dist/index.js +293 -7
- package/dist/{memory-57CD5DYG.js → memory-B4NNFKXV.js} +2 -2
- package/package.json +1 -1
|
@@ -143,6 +143,28 @@ async function runMigrations(db) {
|
|
|
143
143
|
await db.execute(`CREATE INDEX IF NOT EXISTS idx_memories_scope_project ON memories(scope, project_id)`);
|
|
144
144
|
} catch {
|
|
145
145
|
}
|
|
146
|
+
await db.execute(
|
|
147
|
+
`CREATE TABLE IF NOT EXISTS files (
|
|
148
|
+
id TEXT PRIMARY KEY,
|
|
149
|
+
path TEXT NOT NULL,
|
|
150
|
+
content TEXT NOT NULL,
|
|
151
|
+
hash TEXT NOT NULL,
|
|
152
|
+
scope TEXT NOT NULL DEFAULT 'global',
|
|
153
|
+
source TEXT,
|
|
154
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
155
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
156
|
+
deleted_at TEXT,
|
|
157
|
+
UNIQUE(path, scope)
|
|
158
|
+
)`
|
|
159
|
+
);
|
|
160
|
+
try {
|
|
161
|
+
await db.execute(`CREATE INDEX IF NOT EXISTS idx_files_scope ON files(scope)`);
|
|
162
|
+
} catch {
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
await db.execute(`CREATE INDEX IF NOT EXISTS idx_files_path ON files(path)`);
|
|
166
|
+
} catch {
|
|
167
|
+
}
|
|
146
168
|
}
|
|
147
169
|
|
|
148
170
|
export {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
getDb
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-GCWB2OVN.js";
|
|
5
5
|
|
|
6
6
|
// src/lib/memory.ts
|
|
7
7
|
import { nanoid } from "nanoid";
|
|
@@ -99,7 +99,7 @@ async function addMemory(content, opts) {
|
|
|
99
99
|
}
|
|
100
100
|
async function generateEmbeddingAsync(memoryId, content) {
|
|
101
101
|
try {
|
|
102
|
-
const { getEmbedding, storeEmbedding, ensureEmbeddingsSchema } = await import("./embeddings-
|
|
102
|
+
const { getEmbedding, storeEmbedding, ensureEmbeddingsSchema } = await import("./embeddings-6N2NZPRX.js");
|
|
103
103
|
await ensureEmbeddingsSchema();
|
|
104
104
|
const embedding = await getEmbedding(content);
|
|
105
105
|
await storeEmbedding(memoryId, embedding);
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
ensureEmbeddingsSchema,
|
|
4
4
|
getEmbedding,
|
|
5
5
|
storeEmbedding
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-ZSP6BMAR.js";
|
|
7
7
|
import {
|
|
8
8
|
addMemory,
|
|
9
9
|
bulkForgetByIds,
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
listMemories,
|
|
18
18
|
searchMemories,
|
|
19
19
|
updateMemory
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-TRACL3XY.js";
|
|
21
21
|
import {
|
|
22
22
|
getConfigDir,
|
|
23
23
|
getDb,
|
|
@@ -25,10 +25,10 @@ import {
|
|
|
25
25
|
resetDb,
|
|
26
26
|
saveSyncConfig,
|
|
27
27
|
syncDb
|
|
28
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-GCWB2OVN.js";
|
|
29
29
|
|
|
30
30
|
// src/index.ts
|
|
31
|
-
import { Command as
|
|
31
|
+
import { Command as Command29 } from "commander";
|
|
32
32
|
|
|
33
33
|
// src/commands/init.ts
|
|
34
34
|
import { Command } from "commander";
|
|
@@ -634,7 +634,7 @@ var searchCommand = new Command5("search").description("Search memories using fu
|
|
|
634
634
|
}
|
|
635
635
|
if (opts.semantic) {
|
|
636
636
|
try {
|
|
637
|
-
const { semanticSearch, isModelAvailable } = await import("./embeddings-
|
|
637
|
+
const { semanticSearch, isModelAvailable } = await import("./embeddings-6N2NZPRX.js");
|
|
638
638
|
if (!await isModelAvailable()) {
|
|
639
639
|
console.log(chalk6.yellow("\u26A0") + " Loading embedding model for first time (this may take a moment)...");
|
|
640
640
|
}
|
|
@@ -654,7 +654,7 @@ var searchCommand = new Command5("search").description("Search memories using fu
|
|
|
654
654
|
console.log(chalk6.bold(`Semantic results for "${query}":`));
|
|
655
655
|
console.log("");
|
|
656
656
|
for (const r of results) {
|
|
657
|
-
const { getMemoryById: getMemoryById2 } = await import("./memory-
|
|
657
|
+
const { getMemoryById: getMemoryById2 } = await import("./memory-B4NNFKXV.js");
|
|
658
658
|
const m = await getMemoryById2(r.id);
|
|
659
659
|
if (m) {
|
|
660
660
|
console.log(formatMemory2(m, r.score));
|
|
@@ -3599,8 +3599,293 @@ var logoutCommand = new Command27("logout").description("Log out of memories.sh"
|
|
|
3599
3599
|
success("Logged out successfully.");
|
|
3600
3600
|
});
|
|
3601
3601
|
|
|
3602
|
+
// src/commands/files.ts
|
|
3603
|
+
import { Command as Command28 } from "commander";
|
|
3604
|
+
import chalk26 from "chalk";
|
|
3605
|
+
import ora4 from "ora";
|
|
3606
|
+
import { nanoid as nanoid4 } from "nanoid";
|
|
3607
|
+
import { createHash } from "crypto";
|
|
3608
|
+
import { readFile as readFile8, writeFile as writeFile6, mkdir as mkdir4, readdir } from "fs/promises";
|
|
3609
|
+
import { existsSync as existsSync9 } from "fs";
|
|
3610
|
+
import { join as join8, dirname as dirname2 } from "path";
|
|
3611
|
+
import { homedir as homedir4 } from "os";
|
|
3612
|
+
function hashContent(content) {
|
|
3613
|
+
return createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
3614
|
+
}
|
|
3615
|
+
var CONFIG_DIRS = [
|
|
3616
|
+
{ dir: ".agents", name: "Agents" },
|
|
3617
|
+
{ dir: ".claude", name: "Claude" },
|
|
3618
|
+
{ dir: ".cursor", name: "Cursor" },
|
|
3619
|
+
{ dir: ".github/copilot", name: "Copilot" },
|
|
3620
|
+
{ dir: ".windsurf", name: "Windsurf" },
|
|
3621
|
+
{ dir: ".cline", name: "Cline" },
|
|
3622
|
+
{ dir: ".codex", name: "Codex" },
|
|
3623
|
+
{ dir: ".gemini", name: "Gemini" },
|
|
3624
|
+
{ dir: ".roo", name: "Roo" },
|
|
3625
|
+
{ dir: ".amp", name: "Amp" }
|
|
3626
|
+
];
|
|
3627
|
+
var INCLUDE_PATTERNS = [
|
|
3628
|
+
/\.md$/,
|
|
3629
|
+
/\.txt$/,
|
|
3630
|
+
/\.json$/,
|
|
3631
|
+
/\.yaml$/,
|
|
3632
|
+
/\.yml$/,
|
|
3633
|
+
/\.toml$/,
|
|
3634
|
+
/rules$/,
|
|
3635
|
+
/config$/
|
|
3636
|
+
];
|
|
3637
|
+
var EXCLUDE_PATTERNS = [
|
|
3638
|
+
/node_modules/,
|
|
3639
|
+
/\.git\//,
|
|
3640
|
+
/\.DS_Store/,
|
|
3641
|
+
/\.log$/,
|
|
3642
|
+
/cache/i,
|
|
3643
|
+
/history/i,
|
|
3644
|
+
/session/i,
|
|
3645
|
+
/debug/i,
|
|
3646
|
+
/\.lock$/,
|
|
3647
|
+
/stats-cache/,
|
|
3648
|
+
/telemetry/,
|
|
3649
|
+
/todos/
|
|
3650
|
+
];
|
|
3651
|
+
function shouldIncludeFile(filePath) {
|
|
3652
|
+
for (const pattern of EXCLUDE_PATTERNS) {
|
|
3653
|
+
if (pattern.test(filePath)) return false;
|
|
3654
|
+
}
|
|
3655
|
+
for (const pattern of INCLUDE_PATTERNS) {
|
|
3656
|
+
if (pattern.test(filePath)) return true;
|
|
3657
|
+
}
|
|
3658
|
+
return false;
|
|
3659
|
+
}
|
|
3660
|
+
async function scanDirectory(dir, basePath = "") {
|
|
3661
|
+
const files = [];
|
|
3662
|
+
if (!existsSync9(dir)) return files;
|
|
3663
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
3664
|
+
for (const entry of entries) {
|
|
3665
|
+
const fullPath = join8(dir, entry.name);
|
|
3666
|
+
const relativePath = basePath ? join8(basePath, entry.name) : entry.name;
|
|
3667
|
+
if (entry.isDirectory()) {
|
|
3668
|
+
const subFiles = await scanDirectory(fullPath, relativePath);
|
|
3669
|
+
files.push(...subFiles);
|
|
3670
|
+
} else if (entry.isFile() && shouldIncludeFile(relativePath)) {
|
|
3671
|
+
files.push({ path: relativePath, fullPath });
|
|
3672
|
+
}
|
|
3673
|
+
}
|
|
3674
|
+
return files;
|
|
3675
|
+
}
|
|
3676
|
+
var filesCommand = new Command28("files").description("Manage synced config files (.agents, .cursor, .claude, etc.)");
|
|
3677
|
+
filesCommand.command("list").alias("ls").description("List synced files").option("-s, --scope <scope>", "Filter by scope (global or project path)").action(async (opts) => {
|
|
3678
|
+
const db = await getDb();
|
|
3679
|
+
let sql = "SELECT id, path, hash, scope, source, updated_at FROM files WHERE deleted_at IS NULL";
|
|
3680
|
+
const args = [];
|
|
3681
|
+
if (opts.scope) {
|
|
3682
|
+
sql += " AND scope = ?";
|
|
3683
|
+
args.push(opts.scope);
|
|
3684
|
+
}
|
|
3685
|
+
sql += " ORDER BY scope, path";
|
|
3686
|
+
const result = await db.execute({ sql, args });
|
|
3687
|
+
const files = result.rows;
|
|
3688
|
+
if (files.length === 0) {
|
|
3689
|
+
console.log(chalk26.dim("No synced files yet."));
|
|
3690
|
+
console.log(chalk26.dim(`Run ${chalk26.cyan("memories files ingest")} to import config files.`));
|
|
3691
|
+
return;
|
|
3692
|
+
}
|
|
3693
|
+
const byScope = /* @__PURE__ */ new Map();
|
|
3694
|
+
for (const file of files) {
|
|
3695
|
+
const scope = file.scope;
|
|
3696
|
+
if (!byScope.has(scope)) byScope.set(scope, []);
|
|
3697
|
+
byScope.get(scope).push(file);
|
|
3698
|
+
}
|
|
3699
|
+
for (const [scope, scopeFiles] of byScope) {
|
|
3700
|
+
const scopeLabel = scope === "global" ? chalk26.blue("Global") : chalk26.yellow(scope.replace("github.com/", ""));
|
|
3701
|
+
console.log(`
|
|
3702
|
+
${scopeLabel} ${chalk26.dim(`(${scopeFiles.length} files)`)}`);
|
|
3703
|
+
console.log(chalk26.dim("\u2500".repeat(50)));
|
|
3704
|
+
for (const file of scopeFiles) {
|
|
3705
|
+
const source = file.source ? chalk26.dim(` [${file.source}]`) : "";
|
|
3706
|
+
console.log(` ${chalk26.white(file.path)}${source}`);
|
|
3707
|
+
}
|
|
3708
|
+
}
|
|
3709
|
+
console.log();
|
|
3710
|
+
});
|
|
3711
|
+
filesCommand.command("ingest").description("Import files from .agents, .cursor, .claude and other config directories").option("-g, --global", "Ingest global configs from home directory", true).option("-p, --project", "Ingest project configs from current directory").option("--dry-run", "Show what would be imported without making changes").action(async (opts) => {
|
|
3712
|
+
const db = await getDb();
|
|
3713
|
+
const home = homedir4();
|
|
3714
|
+
const cwd = process.cwd();
|
|
3715
|
+
const filesToIngest = [];
|
|
3716
|
+
if (opts.global !== false) {
|
|
3717
|
+
for (const { dir, name } of CONFIG_DIRS) {
|
|
3718
|
+
const globalDir = join8(home, dir);
|
|
3719
|
+
const files = await scanDirectory(globalDir);
|
|
3720
|
+
for (const file of files) {
|
|
3721
|
+
filesToIngest.push({
|
|
3722
|
+
path: join8(dir, file.path),
|
|
3723
|
+
fullPath: file.fullPath,
|
|
3724
|
+
scope: "global",
|
|
3725
|
+
source: name
|
|
3726
|
+
});
|
|
3727
|
+
}
|
|
3728
|
+
}
|
|
3729
|
+
}
|
|
3730
|
+
if (opts.project) {
|
|
3731
|
+
for (const { dir, name } of CONFIG_DIRS) {
|
|
3732
|
+
const projectDir = join8(cwd, dir);
|
|
3733
|
+
const files = await scanDirectory(projectDir);
|
|
3734
|
+
for (const file of files) {
|
|
3735
|
+
filesToIngest.push({
|
|
3736
|
+
path: join8(dir, file.path),
|
|
3737
|
+
fullPath: file.fullPath,
|
|
3738
|
+
scope: "project",
|
|
3739
|
+
// Will be resolved to git remote
|
|
3740
|
+
source: name
|
|
3741
|
+
});
|
|
3742
|
+
}
|
|
3743
|
+
}
|
|
3744
|
+
}
|
|
3745
|
+
if (filesToIngest.length === 0) {
|
|
3746
|
+
console.log(chalk26.dim("No config files found to import."));
|
|
3747
|
+
return;
|
|
3748
|
+
}
|
|
3749
|
+
if (opts.dryRun) {
|
|
3750
|
+
console.log(chalk26.bold(`Would import ${filesToIngest.length} files:
|
|
3751
|
+
`));
|
|
3752
|
+
for (const file of filesToIngest) {
|
|
3753
|
+
const scopeLabel = file.scope === "global" ? chalk26.blue("G") : chalk26.yellow("P");
|
|
3754
|
+
console.log(` ${scopeLabel} ${file.path} ${chalk26.dim(`[${file.source}]`)}`);
|
|
3755
|
+
}
|
|
3756
|
+
return;
|
|
3757
|
+
}
|
|
3758
|
+
const spinner = ora4(`Importing ${filesToIngest.length} files...`).start();
|
|
3759
|
+
let imported = 0;
|
|
3760
|
+
let updated = 0;
|
|
3761
|
+
let skipped = 0;
|
|
3762
|
+
for (const file of filesToIngest) {
|
|
3763
|
+
try {
|
|
3764
|
+
const content = await readFile8(file.fullPath, "utf-8");
|
|
3765
|
+
const hash = hashContent(content);
|
|
3766
|
+
const existing = await db.execute({
|
|
3767
|
+
sql: "SELECT id, hash FROM files WHERE path = ? AND scope = ? AND deleted_at IS NULL",
|
|
3768
|
+
args: [file.path, file.scope]
|
|
3769
|
+
});
|
|
3770
|
+
if (existing.rows.length > 0) {
|
|
3771
|
+
const existingFile = existing.rows[0];
|
|
3772
|
+
if (existingFile.hash === hash) {
|
|
3773
|
+
skipped++;
|
|
3774
|
+
continue;
|
|
3775
|
+
}
|
|
3776
|
+
await db.execute({
|
|
3777
|
+
sql: "UPDATE files SET content = ?, hash = ?, updated_at = datetime('now') WHERE id = ?",
|
|
3778
|
+
args: [content, hash, existingFile.id]
|
|
3779
|
+
});
|
|
3780
|
+
updated++;
|
|
3781
|
+
} else {
|
|
3782
|
+
const id = nanoid4(12);
|
|
3783
|
+
await db.execute({
|
|
3784
|
+
sql: `INSERT INTO files (id, path, content, hash, scope, source) VALUES (?, ?, ?, ?, ?, ?)`,
|
|
3785
|
+
args: [id, file.path, content, hash, file.scope, file.source]
|
|
3786
|
+
});
|
|
3787
|
+
imported++;
|
|
3788
|
+
}
|
|
3789
|
+
} catch (err) {
|
|
3790
|
+
}
|
|
3791
|
+
}
|
|
3792
|
+
const sync = await readSyncConfig();
|
|
3793
|
+
if (sync) {
|
|
3794
|
+
spinner.text = "Syncing to cloud...";
|
|
3795
|
+
await syncDb();
|
|
3796
|
+
}
|
|
3797
|
+
spinner.succeed(`Imported ${imported} new, updated ${updated}, skipped ${skipped} unchanged`);
|
|
3798
|
+
});
|
|
3799
|
+
filesCommand.command("apply").description("Write synced files to disk (restore from cloud)").option("-g, --global", "Apply global files to home directory").option("-p, --project", "Apply project files to current directory").option("--dry-run", "Show what would be written without making changes").option("-f, --force", "Overwrite existing files without prompting").action(async (opts) => {
|
|
3800
|
+
const db = await getDb();
|
|
3801
|
+
const home = homedir4();
|
|
3802
|
+
const cwd = process.cwd();
|
|
3803
|
+
let sql = "SELECT id, path, content, scope, source FROM files WHERE deleted_at IS NULL";
|
|
3804
|
+
const args = [];
|
|
3805
|
+
if (opts.global && !opts.project) {
|
|
3806
|
+
sql += " AND scope = 'global'";
|
|
3807
|
+
} else if (opts.project && !opts.global) {
|
|
3808
|
+
sql += " AND scope != 'global'";
|
|
3809
|
+
}
|
|
3810
|
+
const result = await db.execute({ sql, args });
|
|
3811
|
+
const files = result.rows;
|
|
3812
|
+
if (files.length === 0) {
|
|
3813
|
+
console.log(chalk26.dim("No files to apply."));
|
|
3814
|
+
return;
|
|
3815
|
+
}
|
|
3816
|
+
if (opts.dryRun) {
|
|
3817
|
+
console.log(chalk26.bold(`Would write ${files.length} files:
|
|
3818
|
+
`));
|
|
3819
|
+
for (const file of files) {
|
|
3820
|
+
const baseDir = file.scope === "global" ? home : cwd;
|
|
3821
|
+
const targetPath = join8(baseDir, file.path);
|
|
3822
|
+
const exists = existsSync9(targetPath);
|
|
3823
|
+
const status = exists ? chalk26.yellow("(overwrite)") : chalk26.green("(new)");
|
|
3824
|
+
console.log(` ${targetPath} ${status}`);
|
|
3825
|
+
}
|
|
3826
|
+
return;
|
|
3827
|
+
}
|
|
3828
|
+
const spinner = ora4(`Applying ${files.length} files...`).start();
|
|
3829
|
+
let written = 0;
|
|
3830
|
+
let skippedExisting = 0;
|
|
3831
|
+
for (const file of files) {
|
|
3832
|
+
const baseDir = file.scope === "global" ? home : cwd;
|
|
3833
|
+
const targetPath = join8(baseDir, file.path);
|
|
3834
|
+
if (existsSync9(targetPath) && !opts.force) {
|
|
3835
|
+
const existingContent = await readFile8(targetPath, "utf-8");
|
|
3836
|
+
const existingHash = hashContent(existingContent);
|
|
3837
|
+
const newHash = hashContent(file.content);
|
|
3838
|
+
if (existingHash !== newHash) {
|
|
3839
|
+
skippedExisting++;
|
|
3840
|
+
continue;
|
|
3841
|
+
}
|
|
3842
|
+
}
|
|
3843
|
+
await mkdir4(dirname2(targetPath), { recursive: true });
|
|
3844
|
+
await writeFile6(targetPath, file.content, "utf-8");
|
|
3845
|
+
written++;
|
|
3846
|
+
}
|
|
3847
|
+
if (skippedExisting > 0) {
|
|
3848
|
+
spinner.succeed(`Applied ${written} files, skipped ${skippedExisting} (use --force to overwrite)`);
|
|
3849
|
+
} else {
|
|
3850
|
+
spinner.succeed(`Applied ${written} files`);
|
|
3851
|
+
}
|
|
3852
|
+
});
|
|
3853
|
+
filesCommand.command("show <path>").description("Show content of a synced file").action(async (path) => {
|
|
3854
|
+
const db = await getDb();
|
|
3855
|
+
const result = await db.execute({
|
|
3856
|
+
sql: "SELECT content, scope, source, updated_at FROM files WHERE path = ? AND deleted_at IS NULL",
|
|
3857
|
+
args: [path]
|
|
3858
|
+
});
|
|
3859
|
+
if (result.rows.length === 0) {
|
|
3860
|
+
console.log(chalk26.red(`File not found: ${path}`));
|
|
3861
|
+
return;
|
|
3862
|
+
}
|
|
3863
|
+
const file = result.rows[0];
|
|
3864
|
+
console.log(chalk26.dim(`# ${path}`));
|
|
3865
|
+
console.log(chalk26.dim(`# Scope: ${file.scope} | Source: ${file.source || "unknown"}`));
|
|
3866
|
+
console.log(chalk26.dim(`# Updated: ${file.updated_at}`));
|
|
3867
|
+
console.log(chalk26.dim("\u2500".repeat(50)));
|
|
3868
|
+
console.log(file.content);
|
|
3869
|
+
});
|
|
3870
|
+
filesCommand.command("forget <path>").description("Remove a file from sync (soft delete)").action(async (path) => {
|
|
3871
|
+
const db = await getDb();
|
|
3872
|
+
const result = await db.execute({
|
|
3873
|
+
sql: "UPDATE files SET deleted_at = datetime('now') WHERE path = ? AND deleted_at IS NULL",
|
|
3874
|
+
args: [path]
|
|
3875
|
+
});
|
|
3876
|
+
if (result.rowsAffected === 0) {
|
|
3877
|
+
console.log(chalk26.red(`File not found: ${path}`));
|
|
3878
|
+
return;
|
|
3879
|
+
}
|
|
3880
|
+
const sync = await readSyncConfig();
|
|
3881
|
+
if (sync) {
|
|
3882
|
+
await syncDb();
|
|
3883
|
+
}
|
|
3884
|
+
console.log(chalk26.green(`\u2713 Removed ${path} from sync`));
|
|
3885
|
+
});
|
|
3886
|
+
|
|
3602
3887
|
// src/index.ts
|
|
3603
|
-
var program = new
|
|
3888
|
+
var program = new Command29().name("memories").description("A local-first memory layer for AI agents").version("0.3.0");
|
|
3604
3889
|
program.addCommand(initCommand);
|
|
3605
3890
|
program.addCommand(addCommand);
|
|
3606
3891
|
program.addCommand(recallCommand);
|
|
@@ -3631,6 +3916,7 @@ program.addCommand(templateCommand);
|
|
|
3631
3916
|
program.addCommand(historyCommand);
|
|
3632
3917
|
program.addCommand(revertCommand);
|
|
3633
3918
|
program.addCommand(embedCommand);
|
|
3919
|
+
program.addCommand(filesCommand);
|
|
3634
3920
|
program.addCommand(loginCommand);
|
|
3635
3921
|
program.addCommand(logoutCommand);
|
|
3636
3922
|
program.parse();
|